diff --git a/.github/workflows/create_job.js b/.github/workflows/create_job.js new file mode 100644 index 0000000000..e14f39f828 --- /dev/null +++ b/.github/workflows/create_job.js @@ -0,0 +1,77 @@ +const { CloudTasksClient } = require('@google-cloud/tasks'); +const { v4: uuidv4 } = require('uuid'); + +const client = new CloudTasksClient(); + +const constructPayload = () => { + const jobId = String(uuidv4()); + const commitHash = process.env.COMMIT_HASH; + const branch = process.env.BRANCH; + const classes = process.env.CLASSES; + const pullRequestNumber = process.env.PULL_REQUEST_NUMBER; + + const payloadStructure = { + "jobId": jobId, + "commitHash": commitHash.trim(), + "branch": branch.trim(), + "classes": convertClassesStringToArray(classes), + "pullRequest": pullRequestNumber.trim(), + } + return payloadStructure; +} + +const convertClassesStringToArray = (classes) => { + const classesArray = classes.split(','); + const trimmedClassesArray = classesArray.map(currentClass => currentClass.trim()); + return trimmedClassesArray; +} + +const formatPayload = (payloadStructure) => { + const parsedPayload = JSON.stringify(JSON.stringify(payloadStructure)); + const payload = `{"argument": ${parsedPayload}}`; + console.log(`Payload: ${payload}`); + return payload; +} + +const constructTask = (serviceAccountEmail, payload, url) => { + const task = { + httpRequest: { + httpMethod: 'POST', + url, + oauthToken: { + serviceAccountEmail, + }, + body: Buffer.from(payload).toString('base64'), + }, + }; + return task; +} + +const createRequestBody = (payload) => { + const project = process.env.PROJECT_ID; + const queue = process.env.QUEUE_ID; + const location = process.env.LOCATION; + const url = process.env.WORKFLOW_URL + const serviceAccountEmail = process.env.SERVICE_ACCOUNT_EMAIL; + const requestBody = { + "fullyQualifiedQueueName": client.queuePath(project, location, queue), + "task": constructTask(serviceAccountEmail, payload, url) + } + return requestBody; +} + +const constructRequest = () => { + const payloadStructure = constructPayload(); + const payload = formatPayload(payloadStructure); + const requestBody = createRequestBody(payload); + const request = { parent: requestBody.fullyQualifiedQueueName, task: requestBody.task }; + return request; +} + +async function createHttpTaskWithToken() { + const request = constructRequest(); + const [response] = await client.createTask(request); + const name = response.name; + console.log(`Created task ${name}`); +} +createHttpTaskWithToken(); diff --git a/.github/workflows/invoke_test_runner.yml b/.github/workflows/invoke_test_runner.yml new file mode 100644 index 0000000000..0ebead00d6 --- /dev/null +++ b/.github/workflows/invoke_test_runner.yml @@ -0,0 +1,58 @@ +name: invoke_test_runner +on: + push: + branches: + - master + workflow_dispatch: + inputs: + BRANCH_INPUT: + description: 'Branch' + required: true + COMMIT_HASH_INPUT: + description: 'Commit hash' + required: true + CLASSES_TO_EXECUTE_INPUT: + description: 'Classes to test' + required: false + PULL_REQUEST_NUMBER_INPUT: + description: 'Pull request number' + required: false +env: + PROJECT_ID: ${{ secrets.PROJECT_ID }} + QUEUE_ID: ${{ secrets.QUEUE_ID }} + LOCATION: ${{ secrets.LOCATION }} + SERVICE_ACCOUNT_EMAIL: ${{ secrets.SERVICE_ACCOUNT_EMAIL }} + WORKFLOW_URL: ${{ secrets.WORKFLOW_URL }} + #Payload variables + COMMIT_HASH: ${{ (github.sha) }} + BRANCH: ${{ (github.ref_name) }} + CLASSES: ${{ (github.event.inputs.CLASSES_TO_EXECUTE_INPUT) }} + PULL_REQUEST_NUMBER: ${{ (github.event.inputs.PULL_REQUEST_NUMBER_INPUT) }} + +jobs: + execute_workflow: + if: github.repository == 'graphql-java/graphql-java' + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v3 + - uses: actions/setup-node@v3 + with: + node-version: '14' + - run: npm install --prefix .github/workflows + + - name: Update COMMIT_HASH + if: ${{ github.event_name == 'workflow_dispatch' }} + run: echo "COMMIT_HASH=${{ (github.event.inputs.COMMIT_HASH_INPUT) }} " >> $GITHUB_ENV + + - name: Update BRANCH + if: ${{ github.event_name == 'workflow_dispatch' }} + run: echo "BRANCH=${{ (github.event.inputs.BRANCH_INPUT) }} " >> $GITHUB_ENV + + - id: 'auth' + name: 'Authenticate to Google Cloud' + uses: google-github-actions/auth@v0.4.0 + with: + credentials_json: ${{ secrets.GOOGLE_APPLICATION_CREDENTIALS }} + + - name: Execute JS script + run: node .github/workflows/create_job.js diff --git a/.github/workflows/package.json b/.github/workflows/package.json new file mode 100644 index 0000000000..71ef804476 --- /dev/null +++ b/.github/workflows/package.json @@ -0,0 +1,14 @@ +{ + "name": "workflow-testrunner-tasksenqueuer", + "private": true, + "engines": { + "node": ">=12.0.0" + }, + "files": [ + "*.js" + ], + "dependencies": { + "@google-cloud/tasks": "^3.0.0", + "uuid": "^8.0.0" + } +} diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 486da839ab..41f2c19b01 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -2,7 +2,7 @@ Thanks for contributing to graphql-java! Please be sure that you read the [Code of Conduct](CODE_OF_CONDUCT.md) before contributing to this project and please -create a new Issue and discuss first what your are planning to do for bigger changes. +create a new Issue and discuss first what you are planning to do for larger changes. The overall goal of graphql-java is to have a correct implementation of the [GraphQL Spec](https://github.com/facebook/graphql/) in a production ready way. diff --git a/README.md b/README.md index a0a65e3b81..f1b8e82cb4 100644 --- a/README.md +++ b/README.md @@ -5,25 +5,23 @@ Discuss and ask questions in our Discussions: https://github.com/graphql-java/gr This is a [GraphQL](https://github.com/graphql/graphql-spec) Java implementation. [![Build](https://github.com/graphql-java/graphql-java/actions/workflows/master.yml/badge.svg)](https://github.com/graphql-java/graphql-java/actions/workflows/master.yml) -[![Latest Release](https://img.shields.io/maven-central/v/com.graphql-java/graphql-java?versionPrefix=18)](https://maven-badges.herokuapp.com/maven-central/com.graphql-java/graphql-java/) -[![Latest Snapshot](https://img.shields.io/maven-central/v/com.graphql-java/graphql-java?label=maven-central%20snapshot)](https://maven-badges.herokuapp.com/maven-central/com.graphql-java/graphql-java/) +[![Latest Release](https://img.shields.io/maven-central/v/com.graphql-java/graphql-java?versionPrefix=19)](https://maven-badges.herokuapp.com/maven-central/com.graphql-java/graphql-java/) +[![Latest Snapshot](https://img.shields.io/maven-central/v/com.graphql-java/graphql-java?label=maven-central%20snapshot&versionPrefix=0)](https://maven-badges.herokuapp.com/maven-central/com.graphql-java/graphql-java/) [![MIT licensed](https://img.shields.io/badge/license-MIT-green)](https://github.com/graphql-java/graphql-java/blob/master/LICENSE.md) - ### Documentation We have a tutorial for beginners: [Getting started with GraphQL Java and Spring Boot](https://www.graphql-java.com/tutorials/getting-started-with-spring-boot/) For details how to use `graphql-java` please look at the documentation: https://www.graphql-java.com/documentation/getting-started - Please take a look at our [list of releases](https://github.com/graphql-java/graphql-java/releases) if you want to learn more about new releases and the changelog. ### Code of Conduct Please note that this project is released with a [Contributor Code of Conduct](CODE_OF_CONDUCT.md). By contributing to this project (commenting or opening PR/Issues etc) you are agreeing to follow this conduct, so please -take the time to read it. +take the time to read it. ### License @@ -34,5 +32,3 @@ Copyright (c) 2015, Andreas Marek and [Contributors](https://github.com/graphql- ![YourKit](https://www.yourkit.com/images/yklogo.png) [YourKit](https://www.yourkit.com/) supports this project by providing the YourKit Java Profiler. - - diff --git a/README.zh_cn.md b/README.zh_cn.md index b85937e76f..28190754e6 100644 --- a/README.zh_cn.md +++ b/README.zh_cn.md @@ -5,11 +5,10 @@ 该组件是 [GraphQL 规范](https://github.com/graphql/graphql-spec) 的 Java 实现。 [![Build](https://github.com/graphql-java/graphql-java/actions/workflows/master.yml/badge.svg)](https://github.com/graphql-java/graphql-java/actions/workflows/master.yml) -[![Latest Release](https://img.shields.io/maven-central/v/com.graphql-java/graphql-java?versionPrefix=18)](https://maven-badges.herokuapp.com/maven-central/com.graphql-java/graphql-java/) -[![Latest Snapshot](https://img.shields.io/maven-central/v/com.graphql-java/graphql-java?label=maven-central%20snapshot)](https://maven-badges.herokuapp.com/maven-central/com.graphql-java/graphql-java/) +[![Latest Release](https://img.shields.io/maven-central/v/com.graphql-java/graphql-java?versionPrefix=19)](https://maven-badges.herokuapp.com/maven-central/com.graphql-java/graphql-java/) +[![Latest Snapshot](https://img.shields.io/maven-central/v/com.graphql-java/graphql-java?label=maven-central%20snapshot&versionPrefix=0)](https://maven-badges.herokuapp.com/maven-central/com.graphql-java/graphql-java/) [![MIT licensed](https://img.shields.io/badge/license-MIT-green)](https://github.com/graphql-java/graphql-java/blob/master/LICENSE.md) - ### 文档 入门教程:[Getting started with GraphQL Java and Spring Boot](https://www.graphql-java.com/tutorials/getting-started-with-spring-boot/) diff --git a/build.gradle b/build.gradle index 37dcab18a7..609d59347a 100644 --- a/build.gradle +++ b/build.gradle @@ -7,7 +7,7 @@ plugins { id 'antlr' id 'signing' id "com.github.johnrengelman.shadow" version "7.1.2" - id "biz.aQute.bnd.builder" version "6.1.0" + id "biz.aQute.bnd.builder" version "6.3.1" id "io.github.gradle-nexus.publish-plugin" version "1.1.0" id "groovy" id "me.champeau.jmh" version "0.6.6" diff --git a/settings.gradle b/settings.gradle index 72827aa7ad..160b054c14 100644 --- a/settings.gradle +++ b/settings.gradle @@ -1,2 +1,16 @@ -rootProject.name = 'graphql-java' +pluginManagement { + repositories { + mavenCentral() + maven { + url 'https://plugins.gradle.org/m2' + metadataSources { + // Avoid redirection to defunct JCenter when Gradle module metadata is not published by a plugin (e.g. JMH plugin) + ignoreGradleMetadataRedirection() + mavenPom() + artifact() + } + } + } +} +rootProject.name = 'graphql-java' diff --git a/src/main/java/graphql/DeprecatedAt.java b/src/main/java/graphql/DeprecatedAt.java new file mode 100644 index 0000000000..0918e5f6d6 --- /dev/null +++ b/src/main/java/graphql/DeprecatedAt.java @@ -0,0 +1,22 @@ +package graphql; + +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +import static java.lang.annotation.ElementType.CONSTRUCTOR; +import static java.lang.annotation.ElementType.FIELD; +import static java.lang.annotation.ElementType.METHOD; +import static java.lang.annotation.ElementType.PACKAGE; +import static java.lang.annotation.ElementType.TYPE; + +/** + * Helper to track deprecation + * + * Please use ISO-8601 format i.e. YYYY-MM-DD + */ +@Retention(RetentionPolicy.SOURCE) +@Target(value = {CONSTRUCTOR, METHOD, TYPE, FIELD, PACKAGE}) +public @interface DeprecatedAt { + String value(); +} diff --git a/src/main/java/graphql/Directives.java b/src/main/java/graphql/Directives.java index 7c993d071c..caff79a7d5 100644 --- a/src/main/java/graphql/Directives.java +++ b/src/main/java/graphql/Directives.java @@ -1,14 +1,11 @@ package graphql; -import com.google.common.collect.ImmutableSet; import graphql.language.Description; import graphql.language.DirectiveDefinition; import graphql.language.StringValue; import graphql.schema.GraphQLDirective; -import java.util.Set; - import static graphql.Scalars.GraphQLBoolean; import static graphql.Scalars.GraphQLString; import static graphql.introspection.Introspection.DirectiveLocation.ARGUMENT_DEFINITION; diff --git a/src/main/java/graphql/DirectivesUtil.java b/src/main/java/graphql/DirectivesUtil.java index 1b0401562d..9a3e1fc3ac 100644 --- a/src/main/java/graphql/DirectivesUtil.java +++ b/src/main/java/graphql/DirectivesUtil.java @@ -25,6 +25,7 @@ public class DirectivesUtil { @Deprecated // use GraphQLAppliedDirectives eventually + @DeprecatedAt("2022-02-24") public static Map nonRepeatableDirectivesByName(List directives) { // filter the repeatable directives List singletonDirectives = directives.stream() @@ -34,12 +35,14 @@ public static Map nonRepeatableDirectivesByName(List> allDirectivesByName(List directives) { return ImmutableMap.copyOf(FpKit.groupingBy(directives, GraphQLDirective::getName)); } @Deprecated // use GraphQLAppliedDirectives eventually + @DeprecatedAt("2022-02-24") public static Optional directiveWithArg(List directives, String directiveName, String argumentName) { GraphQLDirective directive = nonRepeatableDirectivesByName(directives).get(directiveName); GraphQLArgument argument = null; @@ -51,6 +54,7 @@ public static Optional directiveWithArg(List @Deprecated // use GraphQLAppliedDirectives eventually + @DeprecatedAt("2022-02-24") public static boolean isAllNonRepeatable(List directives) { if (directives == null || directives.isEmpty()) { return false; @@ -64,6 +68,7 @@ public static boolean isAllNonRepeatable(List directives) { } @Deprecated // use GraphQLAppliedDirectives eventually + @DeprecatedAt("2022-02-24") public static List add(List targetList, GraphQLDirective newDirective) { assertNotNull(targetList, () -> "directive list can't be null"); assertNotNull(newDirective, () -> "directive can't be null"); @@ -72,6 +77,7 @@ public static List add(List targetList, Grap } @Deprecated // use GraphQLAppliedDirectives eventually + @DeprecatedAt("2022-02-24") public static List addAll(List targetList, List newDirectives) { assertNotNull(targetList, () -> "directive list can't be null"); assertNotNull(newDirectives, () -> "directive list can't be null"); @@ -80,6 +86,7 @@ public static List addAll(List targetList, L } @Deprecated // use GraphQLAppliedDirectives eventually + @DeprecatedAt("2022-02-24") public static GraphQLDirective getFirstDirective(String name, Map> allDirectivesByName) { List directives = allDirectivesByName.getOrDefault(name, emptyList()); if (directives.isEmpty()) { diff --git a/src/main/java/graphql/ErrorClassification.java b/src/main/java/graphql/ErrorClassification.java index e23f172810..db9764f2ce 100644 --- a/src/main/java/graphql/ErrorClassification.java +++ b/src/main/java/graphql/ErrorClassification.java @@ -23,4 +23,21 @@ public interface ErrorClassification { default Object toSpecification(GraphQLError error) { return String.valueOf(this); } + + /** + * This produces a simple ErrorClassification that represents the provided String. You can + * use this factory method to give out simple but custom error classifications. + * + * @param errorClassification the string that represents the error classification + * + * @return a ErrorClassification that is that provided string + */ + static ErrorClassification errorClassification(String errorClassification) { + return new ErrorClassification() { + @Override + public String toString() { + return errorClassification; + } + }; + } } diff --git a/src/main/java/graphql/ExecutionInput.java b/src/main/java/graphql/ExecutionInput.java index 6d4ee0036b..4cace2b7b5 100644 --- a/src/main/java/graphql/ExecutionInput.java +++ b/src/main/java/graphql/ExecutionInput.java @@ -44,7 +44,7 @@ private ExecutionInput(Builder builder) { this.dataLoaderRegistry = builder.dataLoaderRegistry; this.cacheControl = builder.cacheControl; this.executionId = builder.executionId; - this.locale = builder.locale; + this.locale = builder.locale != null ? builder.locale : Locale.getDefault(); // always have a locale in place this.localContext = builder.localContext; this.extensions = builder.extensions; } @@ -72,6 +72,7 @@ public String getOperationName() { * @deprecated - use {@link #getGraphQLContext()} */ @Deprecated + @DeprecatedAt("2021-07-05") public Object getContext() { return context; } @@ -124,6 +125,7 @@ public DataLoaderRegistry getDataLoaderRegistry() { * @deprecated - Apollo has deprecated the Cache Control specification */ @Deprecated + @DeprecatedAt("2022-07-26") public CacheControl getCacheControl() { return cacheControl; } @@ -227,6 +229,7 @@ public static class Builder { // dataloader field tracking away. // private DataLoaderRegistry dataLoaderRegistry = DataLoaderDispatcherInstrumentationState.EMPTY_DATALOADER_REGISTRY; + @DeprecatedAt("2022-07-26") private CacheControl cacheControl = CacheControl.newCacheControl(); private Locale locale = Locale.getDefault(); private ExecutionId executionId; @@ -287,6 +290,7 @@ public Builder localContext(Object localContext) { * @deprecated - the {@link ExecutionInput#getGraphQLContext()} is a fixed mutable instance now */ @Deprecated + @DeprecatedAt("2021-07-05") public Builder context(Object context) { this.context = context; return this; @@ -302,6 +306,7 @@ public Builder context(Object context) { * @deprecated - the {@link ExecutionInput#getGraphQLContext()} is a fixed mutable instance now */ @Deprecated + @DeprecatedAt("2021-07-05") public Builder context(GraphQLContext.Builder contextBuilder) { this.context = contextBuilder.build(); return this; @@ -317,6 +322,7 @@ public Builder context(GraphQLContext.Builder contextBuilder) { * @deprecated - the {@link ExecutionInput#getGraphQLContext()} is a fixed mutable instance now */ @Deprecated + @DeprecatedAt("2021-07-05") public Builder context(UnaryOperator contextBuilderFunction) { GraphQLContext.Builder builder = GraphQLContext.newContext(); builder = contextBuilderFunction.apply(builder); @@ -394,6 +400,7 @@ public Builder dataLoaderRegistry(DataLoaderRegistry dataLoaderRegistry) { } @Deprecated + @DeprecatedAt("2022-07-26") public Builder cacheControl(CacheControl cacheControl) { this.cacheControl = assertNotNull(cacheControl); return this; diff --git a/src/main/java/graphql/ExecutionResult.java b/src/main/java/graphql/ExecutionResult.java index e3715ee550..4870144fe5 100644 --- a/src/main/java/graphql/ExecutionResult.java +++ b/src/main/java/graphql/ExecutionResult.java @@ -3,6 +3,7 @@ import java.util.List; import java.util.Map; +import java.util.function.Consumer; /** * This simple value class represents the result of performing a graphql query. @@ -34,7 +35,7 @@ public interface ExecutionResult { * See : https://graphql.github.io/graphql-spec/June2018/#sec-Data * * @return true if the entry "data" should be present in the result - * false otherwise + * false otherwise */ boolean isDataPresent(); @@ -49,9 +50,103 @@ public interface ExecutionResult { * should be present. Certain JSON serializers may or may interpret {@link ExecutionResult} to spec, so this method * is provided to produce a map that strictly follows the specification. * - * See : http://facebook.github.io/graphql/#sec-Response-Format + * See : https://spec.graphql.org/October2021/#sec-Response-Format * * @return a map of the result that strictly follows the spec */ Map toSpecification(); + + + /** + * This helps you transform the current {@link ExecutionResult} object into another one by starting a builder with all + * the current values and allows you to transform it how you want. + * + * @param builderConsumer the consumer code that will be given a builder to transform + * + * @return a new {@link ExecutionResult} object based on calling build on that builder + */ + default ExecutionResult transform(Consumer> builderConsumer) { + Builder builder = newExecutionResult().from(this); + builderConsumer.accept(builder); + return builder.build(); + } + + /** + * @return a builder that allows you to build a new execution result + */ + static Builder newExecutionResult() { + return ExecutionResultImpl.newExecutionResult(); + } + + interface Builder> { + + /** + * Sets values into the builder based on a previous {@link ExecutionResult} + * + * @param executionResult the previous {@link ExecutionResult} + * + * @return the builder + */ + B from(ExecutionResult executionResult); + + /** + * Sets new data into the builder + * + * @param data the data to use + * + * @return the builder + */ + B data(Object data); + + /** + * Sets error list as the errors for this builder + * + * @param errors the errors to use + * + * @return the builder + */ + B errors(List errors); + + /** + * Adds the error list to any existing the errors for this builder + * + * @param errors the errors to add + * + * @return the builder + */ + B addErrors(List errors); + + /** + * Adds the error to any existing the errors for this builder + * + * @param error the error to add + * + * @return the builder + */ + B addError(GraphQLError error); + + /** + * Sets the extension map for this builder + * + * @param extensions the extensions to use + * + * @return the builder + */ + B extensions(Map extensions); + + /** + * Adds a new entry into the extensions map for this builder + * + * @param key the key of the extension entry + * @param value the value of the extension entry + * + * @return the builder + */ + B addExtension(String key, Object value); + + /** + * @return a newly built {@link ExecutionResult} + */ + ExecutionResult build(); + } } diff --git a/src/main/java/graphql/ExecutionResultImpl.java b/src/main/java/graphql/ExecutionResultImpl.java index 1bb675bf07..33ddd67e21 100644 --- a/src/main/java/graphql/ExecutionResultImpl.java +++ b/src/main/java/graphql/ExecutionResultImpl.java @@ -9,7 +9,6 @@ import java.util.LinkedHashMap; import java.util.List; import java.util.Map; -import java.util.function.Consumer; import static graphql.collect.ImmutableKit.map; @@ -104,22 +103,17 @@ public String toString() { '}'; } - public ExecutionResultImpl transform(Consumer builderConsumer) { - Builder builder = newExecutionResult().from(this); - builderConsumer.accept(builder); - return builder.build(); - } - public static Builder newExecutionResult() { return new Builder(); } - public static class Builder { + public static class Builder implements ExecutionResult.Builder { private boolean dataPresent; private Object data; private List errors = new ArrayList<>(); private Map extensions; + @Override public Builder from(ExecutionResult executionResult) { dataPresent = executionResult.isDataPresent(); data = executionResult.getData(); @@ -128,39 +122,46 @@ public Builder from(ExecutionResult executionResult) { return this; } + @Override public Builder data(Object data) { dataPresent = true; this.data = data; return this; } + @Override public Builder errors(List errors) { this.errors = errors; return this; } + @Override public Builder addErrors(List errors) { this.errors.addAll(errors); return this; } + @Override public Builder addError(GraphQLError error) { this.errors.add(error); return this; } + @Override public Builder extensions(Map extensions) { this.extensions = extensions; return this; } + @Override public Builder addExtension(String key, Object value) { this.extensions = (this.extensions == null ? new LinkedHashMap<>() : this.extensions); this.extensions.put(key, value); return this; } - public ExecutionResultImpl build() { + @Override + public ExecutionResult build() { return new ExecutionResultImpl(dataPresent, data, errors, extensions); } } diff --git a/src/main/java/graphql/GraphQL.java b/src/main/java/graphql/GraphQL.java index 0c9e5e70d3..435ed82935 100644 --- a/src/main/java/graphql/GraphQL.java +++ b/src/main/java/graphql/GraphQL.java @@ -17,7 +17,7 @@ import graphql.execution.instrumentation.InstrumentationContext; import graphql.execution.instrumentation.InstrumentationState; import graphql.execution.instrumentation.NoContextChainedInstrumentation; -import graphql.execution.instrumentation.SimpleInstrumentation; +import graphql.execution.instrumentation.SimplePerformantInstrumentation; import graphql.execution.instrumentation.dataloader.DataLoaderDispatcherInstrumentation; import graphql.execution.instrumentation.parameters.InstrumentationCreateStateParameters; import graphql.execution.instrumentation.parameters.InstrumentationExecutionParameters; @@ -35,7 +35,6 @@ import java.util.ArrayList; import java.util.List; import java.util.Locale; -import java.util.Map; import java.util.Optional; import java.util.concurrent.CompletableFuture; import java.util.concurrent.CompletionException; @@ -324,93 +323,6 @@ public ExecutionResult execute(String query) { return execute(executionInput); } - /** - * Info: This sets context = root to be backwards compatible. - * - * @param query the query/mutation/subscription - * @param context custom object provided to each {@link graphql.schema.DataFetcher} - * - * @return an {@link ExecutionResult} which can include errors - * - * @deprecated Use {@link #execute(ExecutionInput)} - */ - @Deprecated - public ExecutionResult execute(String query, Object context) { - ExecutionInput executionInput = ExecutionInput.newExecutionInput() - .query(query) - .context(context) - .root(context) // This we are doing do be backwards compatible - .build(); - return execute(executionInput); - } - - /** - * Info: This sets context = root to be backwards compatible. - * - * @param query the query/mutation/subscription - * @param operationName the name of the operation to execute - * @param context custom object provided to each {@link graphql.schema.DataFetcher} - * - * @return an {@link ExecutionResult} which can include errors - * - * @deprecated Use {@link #execute(ExecutionInput)} - */ - @Deprecated - public ExecutionResult execute(String query, String operationName, Object context) { - ExecutionInput executionInput = ExecutionInput.newExecutionInput() - .query(query) - .operationName(operationName) - .context(context) - .root(context) // This we are doing do be backwards compatible - .build(); - return execute(executionInput); - } - - /** - * Info: This sets context = root to be backwards compatible. - * - * @param query the query/mutation/subscription - * @param context custom object provided to each {@link graphql.schema.DataFetcher} - * @param variables variable values uses as argument - * - * @return an {@link ExecutionResult} which can include errors - * - * @deprecated Use {@link #execute(ExecutionInput)} - */ - @Deprecated - public ExecutionResult execute(String query, Object context, Map variables) { - ExecutionInput executionInput = ExecutionInput.newExecutionInput() - .query(query) - .context(context) - .root(context) // This we are doing do be backwards compatible - .variables(variables) - .build(); - return execute(executionInput); - } - - /** - * Info: This sets context = root to be backwards compatible. - * - * @param query the query/mutation/subscription - * @param operationName name of the operation to execute - * @param context custom object provided to each {@link graphql.schema.DataFetcher} - * @param variables variable values uses as argument - * - * @return an {@link ExecutionResult} which can include errors - * - * @deprecated Use {@link #execute(ExecutionInput)} - */ - @Deprecated - public ExecutionResult execute(String query, String operationName, Object context, Map variables) { - ExecutionInput executionInput = ExecutionInput.newExecutionInput() - .query(query) - .operationName(operationName) - .context(context) - .root(context) // This we are doing do be backwards compatible - .variables(variables) - .build(); - return execute(executionInput); - } /** * Executes the graphql query using the provided input object builder @@ -662,7 +574,7 @@ private CompletableFuture execute(ExecutionInput executionInput private static Instrumentation checkInstrumentationDefaultState(Instrumentation instrumentation, boolean doNotAddDefaultInstrumentations) { if (doNotAddDefaultInstrumentations) { - return instrumentation == null ? SimpleInstrumentation.INSTANCE : instrumentation; + return instrumentation == null ? SimplePerformantInstrumentation.INSTANCE : instrumentation; } if (instrumentation instanceof DataLoaderDispatcherInstrumentation) { return instrumentation; diff --git a/src/main/java/graphql/GraphQLContext.java b/src/main/java/graphql/GraphQLContext.java index 03c2e89d41..081c17725f 100644 --- a/src/main/java/graphql/GraphQLContext.java +++ b/src/main/java/graphql/GraphQLContext.java @@ -224,6 +224,13 @@ public static GraphQLContext of(Consumer contextBuilderC return of(builder.map); } + /** + * @return a new and empty graphql context object + */ + public static GraphQLContext getDefault() { + return GraphQLContext.newContext().build(); + } + /** * Creates a new GraphqlContext builder * diff --git a/src/main/java/graphql/GraphQLError.java b/src/main/java/graphql/GraphQLError.java index 69873b5911..b4fc6fa600 100644 --- a/src/main/java/graphql/GraphQLError.java +++ b/src/main/java/graphql/GraphQLError.java @@ -1,7 +1,9 @@ package graphql; +import graphql.execution.ResultPath; import graphql.language.SourceLocation; +import org.jetbrains.annotations.Nullable; import java.io.Serializable; import java.util.List; @@ -38,7 +40,7 @@ public interface GraphQLError extends Serializable { /** * The graphql spec says that the (optional) path field of any error should be a list - * of path entries - http://facebook.github.io/graphql/#sec-Errors + * of path entries https://spec.graphql.org/October2021/#sec-Handling-Field-Errors * * @return the path in list format */ @@ -66,5 +68,85 @@ default Map getExtensions() { return null; } + /** + * @return a new builder of {@link GraphQLError}s + */ + static Builder newError() { + return new GraphqlErrorBuilder<>(); + } + + /** + * A builder of {@link GraphQLError}s + */ + interface Builder> { + + /** + * Sets the message of the error using {@link String#format(String, Object...)} with the arguments + * + * @param message the message + * @param formatArgs the arguments to use + * + * @return this builder + */ + B message(String message, Object... formatArgs); + /** + * This adds locations to the error + * + * @param locations the locations to add + * + * @return this builder + */ + B locations(@Nullable List locations); + + /** + * This adds a location to the error + * + * @param location the locations to add + * + * @return this builder + */ + B location(@Nullable SourceLocation location); + + /** + * Sets the path of the message + * + * @param path can be null + * + * @return this builder + */ + B path(@Nullable ResultPath path); + + /** + * Sets the path of the message + * + * @param path can be null + * + * @return this builder + */ + B path(@Nullable List path); + + /** + * Sets the {@link ErrorClassification} of the message + * + * @param errorType the error classification to use + * + * @return this builder + */ + B errorType(ErrorClassification errorType); + + /** + * Sets the extensions of the message + * + * @param extensions the extensions to use + * + * @return this builder + */ + B extensions(@Nullable Map extensions); + + /** + * @return a newly built GraphqlError + */ + GraphQLError build(); + } } diff --git a/src/main/java/graphql/GraphqlErrorBuilder.java b/src/main/java/graphql/GraphqlErrorBuilder.java index ee974ea5ee..4cef5beabe 100644 --- a/src/main/java/graphql/GraphqlErrorBuilder.java +++ b/src/main/java/graphql/GraphqlErrorBuilder.java @@ -4,8 +4,8 @@ import graphql.execution.ResultPath; import graphql.language.SourceLocation; import graphql.schema.DataFetchingEnvironment; - import org.jetbrains.annotations.Nullable; + import java.util.ArrayList; import java.util.List; import java.util.Map; @@ -20,7 +20,7 @@ */ @SuppressWarnings("unchecked") @PublicApi -public class GraphqlErrorBuilder> { +public class GraphqlErrorBuilder> implements GraphQLError.Builder { private String message; private List path; diff --git a/src/main/java/graphql/Mutable.java b/src/main/java/graphql/Mutable.java index 9df649d052..5cc1b30e4c 100644 --- a/src/main/java/graphql/Mutable.java +++ b/src/main/java/graphql/Mutable.java @@ -4,12 +4,13 @@ import java.lang.annotation.RetentionPolicy; import java.lang.annotation.Target; +import static java.lang.annotation.ElementType.METHOD; import static java.lang.annotation.ElementType.TYPE; /** * This marks a type as mutable which means after constructing it can be changed. */ @Retention(RetentionPolicy.RUNTIME) -@Target(value = {TYPE}) +@Target(value = {TYPE,METHOD}) public @interface Mutable { } diff --git a/src/main/java/graphql/ParseAndValidate.java b/src/main/java/graphql/ParseAndValidate.java index 63c29edad2..9410a5ec1b 100644 --- a/src/main/java/graphql/ParseAndValidate.java +++ b/src/main/java/graphql/ParseAndValidate.java @@ -3,6 +3,7 @@ import graphql.language.Document; import graphql.parser.InvalidSyntaxException; import graphql.parser.Parser; +import graphql.parser.ParserEnvironment; import graphql.parser.ParserOptions; import graphql.schema.GraphQLSchema; import graphql.validation.ValidationError; @@ -65,7 +66,12 @@ public static ParseAndValidateResult parse(@NotNull ExecutionInput executionInpu // we use the query parser options by default if they are not specified parserOptions = ofNullable(parserOptions).orElse(ParserOptions.getDefaultOperationParserOptions()); Parser parser = new Parser(); - Document document = parser.parseDocument(executionInput.getQuery(), parserOptions); + Locale locale = executionInput.getLocale() == null ? Locale.getDefault() : executionInput.getLocale(); + ParserEnvironment parserEnvironment = ParserEnvironment.newParserEnvironment() + .document(executionInput.getQuery()).parserOptions(parserOptions) + .locale(locale) + .build(); + Document document = parser.parseDocument(parserEnvironment); return ParseAndValidateResult.newResult().document(document).variables(executionInput.getVariables()).build(); } catch (InvalidSyntaxException e) { return ParseAndValidateResult.newResult().syntaxException(e).variables(executionInput.getVariables()).build(); diff --git a/src/main/java/graphql/Scalars.java b/src/main/java/graphql/Scalars.java index 91754f568d..4a72d248ce 100644 --- a/src/main/java/graphql/Scalars.java +++ b/src/main/java/graphql/Scalars.java @@ -13,13 +13,13 @@ * by the graphql specification (Int, Float, String, Boolean and ID) while others are offer because they are common on * Java platforms. *

- * For more info see http://graphql.org/learn/schema/#scalar-types and more specifically http://facebook.github.io/graphql/#sec-Scalars + * For more info see http://graphql.org/learn/schema/#scalar-types and more specifically https://spec.graphql.org/October2021/#sec-Scalars */ @PublicApi public class Scalars { /** - * This represents the "Int" type as defined in the graphql specification : http://facebook.github.io/graphql/#sec-Int + * This represents the "Int" type as defined in the graphql specification : https://spec.graphql.org/October2021/#sec-Int *

* The Int scalar type represents a signed 32‐bit numeric non‐fractional value. */ @@ -27,7 +27,7 @@ public class Scalars { .name("Int").description("Built-in Int").coercing(new GraphqlIntCoercing()).build(); /** - * This represents the "Float" type as defined in the graphql specification : http://facebook.github.io/graphql/#sec-Float + * This represents the "Float" type as defined in the graphql specification : https://spec.graphql.org/October2021/#sec-Float *

* Note: The Float type in GraphQL is equivalent to Double in Java. (double precision IEEE 754) */ @@ -35,19 +35,19 @@ public class Scalars { .name("Float").description("Built-in Float").coercing(new GraphqlFloatCoercing()).build(); /** - * This represents the "String" type as defined in the graphql specification : http://facebook.github.io/graphql/#sec-String + * This represents the "String" type as defined in the graphql specification : https://spec.graphql.org/October2021/#sec-String */ public static final GraphQLScalarType GraphQLString = GraphQLScalarType.newScalar() .name("String").description("Built-in String").coercing(new GraphqlStringCoercing()).build(); /** - * This represents the "Boolean" type as defined in the graphql specification : http://facebook.github.io/graphql/#sec-Boolean + * This represents the "Boolean" type as defined in the graphql specification : https://spec.graphql.org/October2021/#sec-Boolean */ public static final GraphQLScalarType GraphQLBoolean = GraphQLScalarType.newScalar() .name("Boolean").description("Built-in Boolean").coercing(new GraphqlBooleanCoercing()).build(); /** - * This represents the "ID" type as defined in the graphql specification : http://facebook.github.io/graphql/#sec-ID + * This represents the "ID" type as defined in the graphql specification : https://spec.graphql.org/October2021/#sec-ID *

* The ID scalar type represents a unique identifier, often used to re-fetch an object or as the key for a cache. The * ID type is serialized in the same way as a String; however, it is not intended to be human‐readable. While it is diff --git a/src/main/java/graphql/TypeResolutionEnvironment.java b/src/main/java/graphql/TypeResolutionEnvironment.java index 27edffe946..c606fdd5fe 100644 --- a/src/main/java/graphql/TypeResolutionEnvironment.java +++ b/src/main/java/graphql/TypeResolutionEnvironment.java @@ -4,7 +4,6 @@ import graphql.execution.DataFetcherResult; import graphql.execution.MergedField; import graphql.execution.TypeResolutionParameters; -import graphql.schema.DataFetchingEnvironment; import graphql.schema.DataFetchingFieldSelectionSet; import graphql.schema.GraphQLSchema; import graphql.schema.GraphQLType; @@ -89,13 +88,14 @@ public GraphQLSchema getSchema() { /** * Returns the context object set in via {@link ExecutionInput#getContext()} * - * @param to two + * @param the type to cast the result to * * @return the context object * * @deprecated use {@link #getGraphQLContext()} instead */ @Deprecated + @DeprecatedAt("2021-12-27") public T getContext() { //noinspection unchecked return (T) context; @@ -111,11 +111,11 @@ public GraphQLContext getGraphQLContext() { /** * Returns the local context object set in via {@link DataFetcherResult#getLocalContext()} * - * @param to two + * @param the type to cast the result to * * @return the local context object */ - T getLocalContext() { + public T getLocalContext() { //noinspection unchecked return (T) localContext; } diff --git a/src/main/java/graphql/analysis/MaxQueryComplexityInstrumentation.java b/src/main/java/graphql/analysis/MaxQueryComplexityInstrumentation.java index a24191fa4d..4b055d4fd4 100644 --- a/src/main/java/graphql/analysis/MaxQueryComplexityInstrumentation.java +++ b/src/main/java/graphql/analysis/MaxQueryComplexityInstrumentation.java @@ -6,11 +6,12 @@ import graphql.execution.ExecutionContext; import graphql.execution.instrumentation.InstrumentationContext; import graphql.execution.instrumentation.InstrumentationState; -import graphql.execution.instrumentation.SimpleInstrumentation; +import graphql.execution.instrumentation.SimplePerformantInstrumentation; import graphql.execution.instrumentation.parameters.InstrumentationCreateStateParameters; import graphql.execution.instrumentation.parameters.InstrumentationExecuteOperationParameters; import graphql.execution.instrumentation.parameters.InstrumentationValidationParameters; import graphql.validation.ValidationError; +import org.jetbrains.annotations.Nullable; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -21,17 +22,18 @@ import java.util.function.Function; import static graphql.Assert.assertNotNull; +import static graphql.execution.instrumentation.InstrumentationState.ofState; import static graphql.execution.instrumentation.SimpleInstrumentationContext.noOp; import static java.util.Optional.ofNullable; /** * Prevents execution if the query complexity is greater than the specified maxComplexity. - * + *

* Use the {@code Function} parameter to supply a function to perform a custom action when the max complexity * is exceeded. If the function returns {@code true} a {@link AbortExecutionException} is thrown. */ @PublicApi -public class MaxQueryComplexityInstrumentation extends SimpleInstrumentation { +public class MaxQueryComplexityInstrumentation extends SimplePerformantInstrumentation { private static final Logger log = LoggerFactory.getLogger(MaxQueryComplexityInstrumentation.class); @@ -87,25 +89,26 @@ public InstrumentationState createState(InstrumentationCreateStateParameters par return new State(); } + @Override - public InstrumentationContext> beginValidation(InstrumentationValidationParameters parameters) { - State state = parameters.getInstrumentationState(); + public @Nullable InstrumentationContext> beginValidation(InstrumentationValidationParameters parameters, InstrumentationState rawState) { + State state = ofState(rawState); // for API backwards compatibility reasons we capture the validation parameters, so we can put them into QueryComplexityInfo state.instrumentationValidationParameters.set(parameters); return noOp(); } @Override - public InstrumentationContext beginExecuteOperation(InstrumentationExecuteOperationParameters instrumentationExecuteOperationParameters) { - State state = instrumentationExecuteOperationParameters.getInstrumentationState(); + public @Nullable InstrumentationContext beginExecuteOperation(InstrumentationExecuteOperationParameters instrumentationExecuteOperationParameters, InstrumentationState rawState) { + State state = ofState(rawState); QueryTraverser queryTraverser = newQueryTraverser(instrumentationExecuteOperationParameters.getExecutionContext()); Map valuesByParent = new LinkedHashMap<>(); queryTraverser.visitPostOrder(new QueryVisitorStub() { @Override public void visitField(QueryVisitorFieldEnvironment env) { - int childsComplexity = valuesByParent.getOrDefault(env, 0); - int value = calculateComplexity(env, childsComplexity); + int childComplexity = valuesByParent.getOrDefault(env, 0); + int value = calculateComplexity(env, childComplexity); valuesByParent.compute(env.getParentEnvironment(), (key, oldValue) -> ofNullable(oldValue).orElse(0) + value @@ -136,7 +139,7 @@ public void visitField(QueryVisitorFieldEnvironment env) { * @param totalComplexity the complexity of the query * @param maxComplexity the maximum complexity allowed * - * @return a instance of AbortExecutionException + * @return an instance of AbortExecutionException */ protected AbortExecutionException mkAbortException(int totalComplexity, int maxComplexity) { return new AbortExecutionException("maximum query complexity exceeded " + totalComplexity + " > " + maxComplexity); @@ -151,12 +154,12 @@ QueryTraverser newQueryTraverser(ExecutionContext executionContext) { .build(); } - private int calculateComplexity(QueryVisitorFieldEnvironment queryVisitorFieldEnvironment, int childsComplexity) { + private int calculateComplexity(QueryVisitorFieldEnvironment queryVisitorFieldEnvironment, int childComplexity) { if (queryVisitorFieldEnvironment.isTypeNameIntrospectionField()) { return 0; } FieldComplexityEnvironment fieldComplexityEnvironment = convertEnv(queryVisitorFieldEnvironment); - return fieldComplexityCalculator.calculate(fieldComplexityEnvironment, childsComplexity); + return fieldComplexityCalculator.calculate(fieldComplexityEnvironment, childComplexity); } private FieldComplexityEnvironment convertEnv(QueryVisitorFieldEnvironment queryVisitorFieldEnvironment) { diff --git a/src/main/java/graphql/analysis/MaxQueryDepthInstrumentation.java b/src/main/java/graphql/analysis/MaxQueryDepthInstrumentation.java index 3a79f9ee0b..f1cee88228 100644 --- a/src/main/java/graphql/analysis/MaxQueryDepthInstrumentation.java +++ b/src/main/java/graphql/analysis/MaxQueryDepthInstrumentation.java @@ -5,8 +5,10 @@ import graphql.execution.AbortExecutionException; import graphql.execution.ExecutionContext; import graphql.execution.instrumentation.InstrumentationContext; -import graphql.execution.instrumentation.SimpleInstrumentation; +import graphql.execution.instrumentation.InstrumentationState; +import graphql.execution.instrumentation.SimplePerformantInstrumentation; import graphql.execution.instrumentation.parameters.InstrumentationExecuteOperationParameters; +import org.jetbrains.annotations.Nullable; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -16,12 +18,12 @@ /** * Prevents execution if the query depth is greater than the specified maxDepth. - * + *

* Use the {@code Function} parameter to supply a function to perform a custom action when the max depth is * exceeded. If the function returns {@code true} a {@link AbortExecutionException} is thrown. */ @PublicApi -public class MaxQueryDepthInstrumentation extends SimpleInstrumentation { +public class MaxQueryDepthInstrumentation extends SimplePerformantInstrumentation { private static final Logger log = LoggerFactory.getLogger(MaxQueryDepthInstrumentation.class); @@ -49,7 +51,7 @@ public MaxQueryDepthInstrumentation(int maxDepth, Function beginExecuteOperation(InstrumentationExecuteOperationParameters parameters) { + public @Nullable InstrumentationContext beginExecuteOperation(InstrumentationExecuteOperationParameters parameters, InstrumentationState state) { QueryTraverser queryTraverser = newQueryTraverser(parameters.getExecutionContext()); int depth = queryTraverser.reducePreOrder((env, acc) -> Math.max(getPathLength(env.getParentEnvironment()), acc), 0); if (log.isDebugEnabled()) { @@ -73,7 +75,7 @@ public InstrumentationContext beginExecuteOperation(Instrumenta * @param depth the depth of the query * @param maxDepth the maximum depth allowed * - * @return a instance of AbortExecutionException + * @return an instance of AbortExecutionException */ protected AbortExecutionException mkAbortException(int depth, int maxDepth) { return new AbortExecutionException("maximum query depth exceeded " + depth + " > " + maxDepth); diff --git a/src/main/java/graphql/analysis/NodeVisitorWithTypeTracking.java b/src/main/java/graphql/analysis/NodeVisitorWithTypeTracking.java index 93d2371daa..7b65c8e9ea 100644 --- a/src/main/java/graphql/analysis/NodeVisitorWithTypeTracking.java +++ b/src/main/java/graphql/analysis/NodeVisitorWithTypeTracking.java @@ -1,5 +1,6 @@ package graphql.analysis; +import graphql.GraphQLContext; import graphql.Internal; import graphql.execution.CoercedVariables; import graphql.execution.ConditionalNodes; @@ -29,6 +30,7 @@ import graphql.util.TraversalControl; import graphql.util.TraverserContext; +import java.util.Locale; import java.util.Map; import static graphql.Assert.assertNotNull; @@ -151,7 +153,12 @@ public TraversalControl visitField(Field field, TraverserContext context) boolean isTypeNameIntrospectionField = fieldDefinition == schema.getIntrospectionTypenameFieldDefinition(); GraphQLFieldsContainer fieldsContainer = !isTypeNameIntrospectionField ? (GraphQLFieldsContainer) unwrapAll(parentEnv.getOutputType()) : null; GraphQLCodeRegistry codeRegistry = schema.getCodeRegistry(); - Map argumentValues = ValuesResolver.getArgumentValues(codeRegistry, fieldDefinition.getArguments(), field.getArguments(), CoercedVariables.of(variables)); + Map argumentValues = ValuesResolver.getArgumentValues(codeRegistry, + fieldDefinition.getArguments(), + field.getArguments(), + CoercedVariables.of(variables), + GraphQLContext.getDefault(), + Locale.getDefault()); QueryVisitorFieldEnvironment environment = new QueryVisitorFieldEnvironmentImpl(isTypeNameIntrospectionField, field, fieldDefinition, diff --git a/src/main/java/graphql/analysis/QueryTraverser.java b/src/main/java/graphql/analysis/QueryTraverser.java index 69f1a09b53..14d873f599 100644 --- a/src/main/java/graphql/analysis/QueryTraverser.java +++ b/src/main/java/graphql/analysis/QueryTraverser.java @@ -1,5 +1,6 @@ package graphql.analysis; +import graphql.GraphQLContext; import graphql.PublicApi; import graphql.execution.CoercedVariables; import graphql.execution.RawVariables; @@ -20,6 +21,7 @@ import java.util.Collections; import java.util.LinkedHashMap; import java.util.List; +import java.util.Locale; import java.util.Map; import static graphql.Assert.assertNotNull; @@ -70,7 +72,7 @@ private QueryTraverser(GraphQLSchema schema, this.fragmentsByName = getOperationResult.fragmentsByName; this.roots = singletonList(getOperationResult.operationDefinition); this.rootParentType = getRootTypeFromOperation(getOperationResult.operationDefinition); - this.coercedVariables = ValuesResolver.coerceVariableValues(schema, variableDefinitions, rawVariables); + this.coercedVariables = ValuesResolver.coerceVariableValues(schema, variableDefinitions, rawVariables, GraphQLContext.getDefault(), Locale.getDefault()); } private QueryTraverser(GraphQLSchema schema, diff --git a/src/main/java/graphql/analysis/values/ValueTraverser.java b/src/main/java/graphql/analysis/values/ValueTraverser.java new file mode 100644 index 0000000000..1cf7745aaa --- /dev/null +++ b/src/main/java/graphql/analysis/values/ValueTraverser.java @@ -0,0 +1,322 @@ +package graphql.analysis.values; + +import com.google.common.collect.ImmutableList; +import graphql.PublicApi; +import graphql.schema.DataFetchingEnvironment; +import graphql.schema.DataFetchingEnvironmentImpl; +import graphql.schema.GraphQLAppliedDirective; +import graphql.schema.GraphQLAppliedDirectiveArgument; +import graphql.schema.GraphQLArgument; +import graphql.schema.GraphQLEnumType; +import graphql.schema.GraphQLFieldDefinition; +import graphql.schema.GraphQLInputObjectField; +import graphql.schema.GraphQLInputObjectType; +import graphql.schema.GraphQLInputSchemaElement; +import graphql.schema.GraphQLInputType; +import graphql.schema.GraphQLInputValueDefinition; +import graphql.schema.GraphQLList; +import graphql.schema.GraphQLNonNull; +import graphql.schema.GraphQLScalarType; +import graphql.schema.GraphQLTypeUtil; + +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import java.util.stream.Collectors; + +import static graphql.Assert.assertShouldNeverHappen; +import static graphql.Assert.assertTrue; +import static graphql.analysis.values.ValueVisitor.ABSENCE_SENTINEL; + +/** + * This class allows you to traverse a set of input values according to the type system and optional + * change the values present. + *

+ * If you just want to traverse without changing anything, just return the value presented to you and nothing will change. + *

+ * If you want to change a value, perhaps in the presence of a directive say on the containing element, then return + * a new value back in your visitor. + *

+ * This class is intended to be used say inside a DataFetcher, allowing you to change the {@link DataFetchingEnvironment#getArguments()} + * say before further processing. + *

+ * The values passed in are assumed to be valid and coerced. This classes does not check for non nullness say or the right coerced objects given + * the type system. This is assumed to have occurred earlier in the graphql validation phase. This also means if you are not careful you can undo the + * validation that has gone before you. For example, it would be possible to change values that are illegal according to the type system, such as + * null values for non-nullable types say, so you need to be careful. + */ +@PublicApi +public class ValueTraverser { + + private static class InputElements implements ValueVisitor.InputElements { + + private final ImmutableList inputElements; + private final List unwrappedInputElements; + private final GraphQLInputValueDefinition lastElement; + + private InputElements(GraphQLInputSchemaElement startElement) { + this.inputElements = ImmutableList.of(startElement); + this.unwrappedInputElements = ImmutableList.of(startElement); + this.lastElement = startElement instanceof GraphQLInputValueDefinition ? (GraphQLInputValueDefinition) startElement : null; + } + + private InputElements(ImmutableList inputElements) { + this.inputElements = inputElements; + this.unwrappedInputElements = inputElements.stream() + .filter(it -> !(it instanceof GraphQLNonNull || it instanceof GraphQLList)) + .collect(ImmutableList.toImmutableList()); + + List inputValDefs = unwrappedInputElements.stream() + .filter(it -> it instanceof GraphQLInputValueDefinition) + .map(GraphQLInputValueDefinition.class::cast).collect(Collectors.toList()); + this.lastElement = inputValDefs.isEmpty() ? null : inputValDefs.get(inputValDefs.size() - 1); + } + + + private InputElements push(GraphQLInputSchemaElement inputElement) { + ImmutableList newSchemaElements = ImmutableList.builder() + .addAll(inputElements).add(inputElement).build(); + return new InputElements(newSchemaElements); + } + + @Override + public List getInputElements() { + return inputElements; + } + + public List getUnwrappedInputElements() { + return unwrappedInputElements; + } + + @Override + public GraphQLInputValueDefinition getLastInputValueDefinition() { + return lastElement; + } + } + + /** + * This will visit the arguments of a {@link DataFetchingEnvironment} and if the values are changed by the visitor a new environment will be built + * + * @param environment the starting data fetching environment + * @param visitor the visitor to use + * + * @return the same environment if nothing changes or a new one with the {@link DataFetchingEnvironment#getArguments()} changed + */ + public static DataFetchingEnvironment visitPreOrder(DataFetchingEnvironment environment, ValueVisitor visitor) { + GraphQLFieldDefinition fieldDefinition = environment.getFieldDefinition(); + Map originalArgs = environment.getArguments(); + Map newArgs = visitPreOrder(originalArgs, fieldDefinition, visitor); + if (newArgs != originalArgs) { + return DataFetchingEnvironmentImpl.newDataFetchingEnvironment(environment).arguments(newArgs).build(); + } + return environment; + } + + /** + * This will visit the arguments of a {@link GraphQLFieldDefinition} and if the visitor changes the values, it will return a new set of arguments + * + * @param coercedArgumentValues the starting coerced arguments + * @param fieldDefinition the field definition + * @param visitor the visitor to use + * + * @return the same set of arguments if nothing changes or new ones if the visitor changes anything + */ + public static Map visitPreOrder(Map coercedArgumentValues, GraphQLFieldDefinition fieldDefinition, ValueVisitor visitor) { + List fieldArguments = fieldDefinition.getArguments(); + boolean copied = false; + for (GraphQLArgument fieldArgument : fieldArguments) { + String key = fieldArgument.getName(); + Object argValue = coercedArgumentValues.get(key); + InputElements inputElements = new InputElements(fieldArgument); + Object newValue = visitor.visitArgumentValue(argValue, fieldArgument, inputElements); + if (hasChanged(newValue, argValue)) { + if (!copied) { + coercedArgumentValues = new LinkedHashMap<>(coercedArgumentValues); + copied = true; + } + setNewValue(coercedArgumentValues, key, newValue); + } + if (newValue != ABSENCE_SENTINEL) { + newValue = visitPreOrderImpl(argValue, fieldArgument.getType(), inputElements, visitor); + if (hasChanged(newValue, argValue)) { + if (!copied) { + coercedArgumentValues = new LinkedHashMap<>(coercedArgumentValues); + copied = true; + } + setNewValue(coercedArgumentValues, key, newValue); + } + } + } + return coercedArgumentValues; + } + + /** + * This will visit a single argument of a {@link GraphQLArgument} and if the visitor changes the value, it will return a new argument value + *

+ * Note you cannot return the ABSENCE_SENTINEL from this method as its makes no sense to be somehow make the argument disappear. Use + * {@link #visitPreOrder(Map, GraphQLFieldDefinition, ValueVisitor)} say to remove arguments in the fields map of arguments. + * + * @param coercedArgumentValue the starting coerced argument value + * @param argument the argument definition + * @param visitor the visitor to use + * + * @return the same value if nothing changes or a new value if the visitor changes anything + */ + public static Object visitPreOrder(Object coercedArgumentValue, GraphQLArgument argument, ValueVisitor visitor) { + InputElements inputElements = new InputElements(argument); + Object newValue = visitor.visitArgumentValue(coercedArgumentValue, argument, inputElements); + if (newValue == ABSENCE_SENTINEL) { + assertShouldNeverHappen("It makes no sense to return the ABSENCE_SENTINEL during the visitPreOrder GraphQLArgument method"); + } + newValue = visitPreOrderImpl(newValue, argument.getType(), inputElements, visitor); + if (newValue == ABSENCE_SENTINEL) { + assertShouldNeverHappen("It makes no sense to return the ABSENCE_SENTINEL during the visitPreOrder GraphQLArgument method"); + } + return newValue; + } + + /** + * This will visit a single argument of a {@link GraphQLAppliedDirective} and if the visitor changes the value, it will return a new argument value + *

+ * Note you cannot return the ABSENCE_SENTINEL from this method as its makes no sense to be somehow make the argument disappear. + * + * @param coercedArgumentValue the starting coerced argument value + * @param argument the applied argument + * @param visitor the visitor to use + * + * @return the same value if nothing changes or a new value if the visitor changes anything + */ + public static Object visitPreOrder(Object coercedArgumentValue, GraphQLAppliedDirectiveArgument argument, ValueVisitor visitor) { + InputElements inputElements = new InputElements(argument); + Object newValue = visitor.visitAppliedDirectiveArgumentValue(coercedArgumentValue, argument, inputElements); + if (newValue == ABSENCE_SENTINEL) { + assertShouldNeverHappen("It makes no sense to return the ABSENCE_SENTINEL during the visitPreOrder GraphQLAppliedDirectiveArgument method"); + } + newValue = visitPreOrderImpl(newValue, argument.getType(), inputElements, visitor); + if (newValue == ABSENCE_SENTINEL) { + assertShouldNeverHappen("It makes no sense to return the ABSENCE_SENTINEL during the visitPreOrder GraphQLAppliedDirectiveArgument method"); + } + return newValue; + } + + private static Object visitPreOrderImpl(Object coercedValue, GraphQLInputType startingInputType, InputElements containingElements, ValueVisitor visitor) { + if (startingInputType instanceof GraphQLNonNull) { + containingElements = containingElements.push(startingInputType); + } + GraphQLInputType inputType = GraphQLTypeUtil.unwrapNonNullAs(startingInputType); + containingElements = containingElements.push(inputType); + if (inputType instanceof GraphQLList) { + return visitListValue(coercedValue, (GraphQLList) inputType, containingElements, visitor); + } else if (inputType instanceof GraphQLInputObjectType) { + GraphQLInputObjectType inputObjectType = (GraphQLInputObjectType) inputType; + return visitObjectValue(coercedValue, inputObjectType, containingElements, visitor); + } else if (inputType instanceof GraphQLScalarType) { + return visitor.visitScalarValue(coercedValue, (GraphQLScalarType) inputType, containingElements); + } else if (inputType instanceof GraphQLEnumType) { + return visitor.visitEnumValue(coercedValue, (GraphQLEnumType) inputType, containingElements); + } else { + return assertShouldNeverHappen("ValueTraverser can only be called on full materialised schemas"); + } + } + + private static Object visitObjectValue(Object coercedValue, GraphQLInputObjectType inputObjectType, InputElements containingElements, ValueVisitor visitor) { + if (coercedValue != null) { + assertTrue(coercedValue instanceof Map, () -> "A input object type MUST have an Map value"); + } + @SuppressWarnings("unchecked") + Map map = (Map) coercedValue; + Map newMap = visitor.visitInputObjectValue(map, inputObjectType, containingElements); + if (newMap == ABSENCE_SENTINEL) { + return ABSENCE_SENTINEL; + } + if (newMap != null) { + boolean copied = false; + for (Map.Entry entry : newMap.entrySet()) { + String key = entry.getKey(); + GraphQLInputObjectField inputField = inputObjectType.getField(key); + /// should we assert if the map contain a key that's not a field ? + if (inputField != null) { + InputElements inputElementsWithField = containingElements.push(inputField); + Object newValue = visitor.visitInputObjectFieldValue(entry.getValue(), inputObjectType, inputField, inputElementsWithField); + if (hasChanged(newValue, entry.getValue())) { + if (!copied) { + newMap = new LinkedHashMap<>(newMap); + copied = true; + } + setNewValue(newMap, key, newValue); + } + // if the value has gone - then we cant descend into it + if (newValue != ABSENCE_SENTINEL) { + newValue = visitPreOrderImpl(newValue, inputField.getType(), inputElementsWithField, visitor); + if (hasChanged(newValue, entry.getValue())) { + if (!copied) { + newMap = new LinkedHashMap<>(newMap); + copied = true; + } + setNewValue(newMap, key, newValue); + } + } + } + } + return newMap; + } else { + return null; + } + } + + private static Object visitListValue(Object coercedValue, GraphQLList listInputType, InputElements containingElements, ValueVisitor visitor) { + if (coercedValue != null) { + assertTrue(coercedValue instanceof List, () -> "A list type MUST have an List value"); + } + @SuppressWarnings("unchecked") + List list = (List) coercedValue; + List newList = visitor.visitListValue(list, listInputType, containingElements); + if (newList == ABSENCE_SENTINEL) { + return ABSENCE_SENTINEL; + } + if (newList != null) { + GraphQLInputType inputType = GraphQLTypeUtil.unwrapOneAs(listInputType); + ImmutableList.Builder copiedList = null; + int i = 0; + for (Object subValue : newList) { + Object newValue = visitPreOrderImpl(subValue, inputType, containingElements, visitor); + if (copiedList != null) { + if (newValue != ABSENCE_SENTINEL) { + copiedList.add(newValue); + } + } else if (hasChanged(newValue, subValue)) { + // go into copy mode because something has changed + // copy previous values up to this point + copiedList = ImmutableList.builder(); + for (int j = 0; j < i; j++) { + copiedList.add(newList.get(j)); + } + if (newValue != ABSENCE_SENTINEL) { + copiedList.add(newValue); + } + } + i++; + } + if (copiedList != null) { + return copiedList.build(); + } else { + return newList; + } + } else { + return null; + } + } + + private static boolean hasChanged(Object newValue, Object oldValue) { + return newValue != oldValue || newValue == ABSENCE_SENTINEL; + } + + private static void setNewValue(Map newMap, String key, Object newValue) { + if (newValue == ABSENCE_SENTINEL) { + newMap.remove(key); + } else { + newMap.put(key, newValue); + } + } + +} diff --git a/src/main/java/graphql/analysis/values/ValueVisitor.java b/src/main/java/graphql/analysis/values/ValueVisitor.java new file mode 100644 index 0000000000..21ae97c0a1 --- /dev/null +++ b/src/main/java/graphql/analysis/values/ValueVisitor.java @@ -0,0 +1,156 @@ +package graphql.analysis.values; + +import graphql.PublicSpi; +import graphql.schema.GraphQLAppliedDirectiveArgument; +import graphql.schema.GraphQLArgument; +import graphql.schema.GraphQLEnumType; +import graphql.schema.GraphQLInputObjectField; +import graphql.schema.GraphQLInputObjectType; +import graphql.schema.GraphQLInputSchemaElement; +import graphql.schema.GraphQLInputValueDefinition; +import graphql.schema.GraphQLList; +import graphql.schema.GraphQLScalarType; +import org.jetbrains.annotations.Nullable; + +import java.util.List; +import java.util.Map; + +/** + * A visitor callback used by {@link ValueTraverser} + */ +@PublicSpi +public interface ValueVisitor { + + /** + * This magic sentinel value indicates that a value should be removed from a list or object versus being set to null, + * that is the difference between a value not being present and a value being null + */ + Object ABSENCE_SENTINEL = new Object() { + @Override + public String toString() { + return "ABSENCE_SENTINEL"; + } + }; + + /** + * Represents the elements that leads to a value and type + */ + interface InputElements { + + /** + * @return then list of input schema elements that lead to an input value. + */ + List getInputElements(); + + /** + * This is the list of input schema elements that are unwrapped, e.g. + * {@link GraphQLList} and {@link graphql.schema.GraphQLNonNull} types have been removed + * + * @return then list of {@link GraphQLInputValueDefinition} elements that lead to an input value. + */ + List getUnwrappedInputElements(); + + /** + * This is the last {@link GraphQLInputValueDefinition} that pointed to the value during a callback. This will + * be either a {@link graphql.schema.GraphQLArgument} or a {@link GraphQLInputObjectField} + * + * @return the last {@link GraphQLInputValueDefinition} that contains this value + */ + GraphQLInputValueDefinition getLastInputValueDefinition(); + } + + /** + * This is called when a scalar value is encountered + * + * @param coercedValue the value that is in coerced form + * @param inputType the type of scalar + * @param inputElements the elements that lead to this value and type + * + * @return the same value or a new value + */ + default @Nullable Object visitScalarValue(@Nullable Object coercedValue, GraphQLScalarType inputType, InputElements inputElements) { + return coercedValue; + } + + /** + * This is called when an enum value is encountered + * + * @param coercedValue the value that is in coerced form + * @param inputType the type of enum + * @param inputElements the elements that lead to this value and type + * + * @return the same value or a new value + */ + default @Nullable Object visitEnumValue(@Nullable Object coercedValue, GraphQLEnumType inputType, InputElements inputElements) { + return coercedValue; + } + + /** + * This is called when an input object field value is encountered + * + * @param coercedValue the value that is in coerced form + * @param inputObjectType the input object type containing the input field + * @param inputObjectField the input object field + * @param inputElements the elements that lead to this value and type + * + * @return the same value or a new value + */ + default @Nullable Object visitInputObjectFieldValue(@Nullable Object coercedValue, GraphQLInputObjectType inputObjectType, GraphQLInputObjectField inputObjectField, InputElements inputElements) { + return coercedValue; + } + + /** + * This is called when an input object value is encountered. + * + * @param coercedValue the value that is in coerced form + * @param inputObjectType the input object type + * @param inputElements the elements that lead to this value and type + * + * @return the same value or a new value + */ + default @Nullable Map visitInputObjectValue(@Nullable Map coercedValue, GraphQLInputObjectType inputObjectType, InputElements inputElements) { + return coercedValue; + } + + /** + * This is called when an input list value is encountered. + * + * @param coercedValue the value that is in coerced form + * @param listInputType the input list type + * @param inputElements the elements that lead to this value and type + * + * @return the same value or a new value + */ + default @Nullable List visitListValue(@Nullable List coercedValue, GraphQLList listInputType, InputElements inputElements) { + return coercedValue; + } + + + /** + * This is called when a {@link GraphQLArgument} is encountered + * + * @param coercedValue the value that is in coerced form + * @param graphQLArgument the {@link GraphQLArgument} in play + * @param inputElements the elements that lead to this value and type + * + * @return the same value or a new value + */ + default @Nullable Object visitArgumentValue(@Nullable Object coercedValue, GraphQLArgument graphQLArgument, InputElements inputElements) { + return coercedValue; + } + + + /** + * This is called when a {@link GraphQLAppliedDirectiveArgument} is encountered + * + * @param coercedValue the value that is in coerced form + * @param graphQLAppliedDirectiveArgument the {@link GraphQLAppliedDirectiveArgument} in play + * @param inputElements the elements that lead to this value and type + * + * @return the same value or a new value + */ + default @Nullable Object visitAppliedDirectiveArgumentValue(@Nullable Object coercedValue, GraphQLAppliedDirectiveArgument graphQLAppliedDirectiveArgument, InputElements inputElements) { + return coercedValue; + } + +} diff --git a/src/main/java/graphql/cachecontrol/CacheControl.java b/src/main/java/graphql/cachecontrol/CacheControl.java index 602bec9d03..7b12b2fb0d 100644 --- a/src/main/java/graphql/cachecontrol/CacheControl.java +++ b/src/main/java/graphql/cachecontrol/CacheControl.java @@ -1,8 +1,8 @@ package graphql.cachecontrol; +import graphql.DeprecatedAt; import graphql.ExecutionInput; import graphql.ExecutionResult; -import graphql.ExecutionResultImpl; import graphql.PublicApi; import graphql.execution.ResultPath; import graphql.schema.DataFetchingEnvironment; @@ -34,6 +34,7 @@ * extensions map as per the specification. */ @Deprecated +@DeprecatedAt("2022-07-26") @PublicApi public class CacheControl { @@ -91,6 +92,7 @@ private CacheControl() { * @deprecated - Apollo has deprecated the Cache Control specification */ @Deprecated + @DeprecatedAt("2022-07-26") public CacheControl hint(ResultPath path, Integer maxAge, Scope scope) { assertNotNull(path); assertNotNull(scope); @@ -108,6 +110,7 @@ public CacheControl hint(ResultPath path, Integer maxAge, Scope scope) { * @deprecated - Apollo has deprecated the Cache Control specification */ @Deprecated + @DeprecatedAt("2022-07-26") public CacheControl hint(ResultPath path, Scope scope) { return hint(path, null, scope); } @@ -122,6 +125,7 @@ public CacheControl hint(ResultPath path, Scope scope) { * @deprecated - Apollo has deprecated the Cache Control specification */ @Deprecated + @DeprecatedAt("2022-07-26") public CacheControl hint(ResultPath path, Integer maxAge) { return hint(path, maxAge, Scope.PUBLIC); } @@ -137,6 +141,7 @@ public CacheControl hint(ResultPath path, Integer maxAge) { * @deprecated - Apollo has deprecated the Cache Control specification */ @Deprecated + @DeprecatedAt("2022-07-26") public CacheControl hint(DataFetchingEnvironment dataFetchingEnvironment, Integer maxAge, Scope scope) { assertNotNull(dataFetchingEnvironment); assertNotNull(scope); @@ -154,6 +159,7 @@ public CacheControl hint(DataFetchingEnvironment dataFetchingEnvironment, Intege * @deprecated - Apollo has deprecated the Cache Control specification */ @Deprecated + @DeprecatedAt("2022-07-26") public CacheControl hint(DataFetchingEnvironment dataFetchingEnvironment, Integer maxAge) { hint(dataFetchingEnvironment, maxAge, Scope.PUBLIC); return this; @@ -169,6 +175,7 @@ public CacheControl hint(DataFetchingEnvironment dataFetchingEnvironment, Intege * @deprecated - Apollo has deprecated the Cache Control specification */ @Deprecated + @DeprecatedAt("2022-07-26") public CacheControl hint(DataFetchingEnvironment dataFetchingEnvironment, Scope scope) { return hint(dataFetchingEnvironment, null, scope); } @@ -181,6 +188,7 @@ public CacheControl hint(DataFetchingEnvironment dataFetchingEnvironment, Scope * @deprecated - Apollo has deprecated the Cache Control specification */ @Deprecated + @DeprecatedAt("2022-07-26") public static CacheControl newCacheControl() { return new CacheControl(); } @@ -195,8 +203,9 @@ public static CacheControl newCacheControl() { * @deprecated - Apollo has deprecated the Cache Control specification */ @Deprecated + @DeprecatedAt("2022-07-26") public ExecutionResult addTo(ExecutionResult executionResult) { - return ExecutionResultImpl.newExecutionResult() + return ExecutionResult.newExecutionResult() .from(executionResult) .addExtension(CACHE_CONTROL_EXTENSION_KEY, hintsToCacheControlProperties()) .build(); diff --git a/src/main/java/graphql/collect/ImmutableKit.java b/src/main/java/graphql/collect/ImmutableKit.java index 6708696958..af74316b69 100644 --- a/src/main/java/graphql/collect/ImmutableKit.java +++ b/src/main/java/graphql/collect/ImmutableKit.java @@ -27,39 +27,10 @@ public static ImmutableMap emptyMap() { return ImmutableMap.of(); } - /** - * ImmutableMaps are hard to build via {@link Map#computeIfAbsent(Object, Function)} style. This method - * allows you to take a mutable map with mutable list of keys and make it immutable. - *

- * This of course has a cost - if the map is very large you will be using more memory. But for static - * maps that live a long life it maybe be worth it. - * - * @param startingMap the starting input map - * @param for key - * @param for victory - * - * @return and Immutable map of ImmutableList values - */ - - public static ImmutableMap> toImmutableMapOfLists(Map> startingMap) { - assertNotNull(startingMap); - ImmutableMap.Builder> map = ImmutableMap.builder(); - for (Map.Entry> e : startingMap.entrySet()) { - ImmutableList value = ImmutableList.copyOf(startingMap.getOrDefault(e.getKey(), emptyList())); - map.put(e.getKey(), value); - } - return map.build(); - } - - public static ImmutableMap addToMap(Map existing, K newKey, V newVal) { return ImmutableMap.builder().putAll(existing).put(newKey, newVal).build(); } - public static ImmutableMap mergeMaps(Map m1, Map m2) { - return ImmutableMap.builder().putAll(m1).putAll(m2).build(); - } - public static ImmutableList concatLists(List l1, List l2) { //noinspection UnstableApiUsage return ImmutableList.builderWithExpectedSize(l1.size() + l2.size()).addAll(l1).addAll(l2).build(); @@ -123,6 +94,7 @@ public static ImmutableList mapAndDropNulls(Iterable iter * * @return an Immutable list with the extra items. */ + @SafeVarargs public static ImmutableList addToList(Collection existing, T newValue, T... extraValues) { assertNotNull(existing); assertNotNull(newValue); @@ -147,6 +119,7 @@ public static ImmutableList addToList(Collection existing, T * * @return an Immutable Set with the extra items. */ + @SafeVarargs public static ImmutableSet addToSet(Collection existing, T newValue, T... extraValues) { assertNotNull(existing); assertNotNull(newValue); diff --git a/src/main/java/graphql/collect/ImmutableMapWithNullValues.java b/src/main/java/graphql/collect/ImmutableMapWithNullValues.java index ed046098c3..207ad9b5d3 100644 --- a/src/main/java/graphql/collect/ImmutableMapWithNullValues.java +++ b/src/main/java/graphql/collect/ImmutableMapWithNullValues.java @@ -1,6 +1,7 @@ package graphql.collect; import graphql.Assert; +import graphql.DeprecatedAt; import graphql.Internal; import java.util.Collection; @@ -82,24 +83,28 @@ public V get(Object key) { @Override @Deprecated + @DeprecatedAt("2020-11-10") public V put(K key, V value) { throw new UnsupportedOperationException(); } @Override @Deprecated + @DeprecatedAt("2020-11-10") public V remove(Object key) { throw new UnsupportedOperationException(); } @Override @Deprecated + @DeprecatedAt("2020-11-10") public void putAll(Map m) { throw new UnsupportedOperationException(); } @Override @Deprecated + @DeprecatedAt("2020-11-10") public void clear() { throw new UnsupportedOperationException(); } @@ -141,54 +146,63 @@ public void forEach(BiConsumer action) { @Override @Deprecated + @DeprecatedAt("2020-11-10") public void replaceAll(BiFunction function) { throw new UnsupportedOperationException(); } @Override @Deprecated + @DeprecatedAt("2020-11-10") public V putIfAbsent(K key, V value) { throw new UnsupportedOperationException(); } @Override @Deprecated + @DeprecatedAt("2020-11-10") public boolean remove(Object key, Object value) { throw new UnsupportedOperationException(); } @Override @Deprecated + @DeprecatedAt("2020-11-10") public boolean replace(K key, V oldValue, V newValue) { throw new UnsupportedOperationException(); } @Override @Deprecated + @DeprecatedAt("2020-11-10") public V replace(K key, V value) { throw new UnsupportedOperationException(); } @Override @Deprecated + @DeprecatedAt("2020-11-10") public V computeIfAbsent(K key, Function mappingFunction) { throw new UnsupportedOperationException(); } @Override @Deprecated + @DeprecatedAt("2020-11-10") public V computeIfPresent(K key, BiFunction remappingFunction) { throw new UnsupportedOperationException(); } @Override @Deprecated + @DeprecatedAt("2020-11-10") public V compute(K key, BiFunction remappingFunction) { throw new UnsupportedOperationException(); } @Override @Deprecated + @DeprecatedAt("2020-11-10") public V merge(K key, V value, BiFunction remappingFunction) { throw new UnsupportedOperationException(); } diff --git a/src/main/java/graphql/execution/Async.java b/src/main/java/graphql/execution/Async.java index b1dd37b527..cc2f631401 100644 --- a/src/main/java/graphql/execution/Async.java +++ b/src/main/java/graphql/execution/Async.java @@ -2,7 +2,6 @@ import graphql.Assert; import graphql.Internal; -import graphql.collect.ImmutableKit; import java.util.ArrayList; import java.util.Collection; @@ -13,7 +12,6 @@ import java.util.concurrent.CompletionException; import java.util.concurrent.CompletionStage; import java.util.function.BiFunction; -import java.util.function.Function; import java.util.function.Supplier; @Internal @@ -28,7 +26,7 @@ public interface CombinedBuilder { } /** - * Combines 1 or more CF. It is a wrapper around CompletableFuture.allOf. + * Combines 0 or more CF into one. It is a wrapper around CompletableFuture.allOf. * * @param expectedSize how many we expect * @param for two @@ -135,29 +133,8 @@ public interface CFFactory { CompletableFuture apply(T input, int index, List previousResults); } - public static CompletableFuture> each(List> futures) { - CompletableFuture> overallResult = new CompletableFuture<>(); - - @SuppressWarnings("unchecked") - CompletableFuture[] arrayOfFutures = futures.toArray(new CompletableFuture[0]); - CompletableFuture - .allOf(arrayOfFutures) - .whenComplete((ignored, exception) -> { - if (exception != null) { - overallResult.completeExceptionally(exception); - return; - } - List results = new ArrayList<>(arrayOfFutures.length); - for (CompletableFuture future : arrayOfFutures) { - results.add(future.join()); - } - overallResult.complete(results); - }); - return overallResult; - } - public static CompletableFuture> each(Collection list, BiFunction> cfFactory) { - List> futures = new ArrayList<>(list.size()); + CombinedBuilder futures = ofExpectedSize(list.size()); int index = 0; for (T t : list) { CompletableFuture cf; @@ -171,8 +148,7 @@ public static CompletableFuture> each(Collection list, BiFunct } futures.add(cf); } - return each(futures); - + return futures.await(); } public static CompletableFuture> eachSequentially(Iterable list, CFFactory cfFactory) { @@ -238,21 +214,4 @@ public static CompletableFuture exceptionallyCompletedFuture(Throwable ex return result; } - public static CompletableFuture> flatMap(List inputs, Function> mapper) { - List> collect = ImmutableKit.map(inputs, mapper); - return Async.each(collect); - } - - public static CompletableFuture> map(CompletableFuture> values, Function mapper) { - return values.thenApply(list -> ImmutableKit.map(list, mapper)); - } - - public static List> map(List> values, Function mapper) { - return ImmutableKit.map(values, cf -> cf.thenApply(mapper)); - } - - public static List> mapCompose(List> values, Function> mapper) { - return ImmutableKit.map(values, cf -> cf.thenCompose(mapper)); - } - } diff --git a/src/main/java/graphql/execution/AsyncExecutionStrategy.java b/src/main/java/graphql/execution/AsyncExecutionStrategy.java index bdac02f5a9..fdebcb6cca 100644 --- a/src/main/java/graphql/execution/AsyncExecutionStrategy.java +++ b/src/main/java/graphql/execution/AsyncExecutionStrategy.java @@ -6,9 +6,7 @@ import graphql.execution.instrumentation.Instrumentation; import graphql.execution.instrumentation.parameters.InstrumentationExecutionStrategyParameters; -import java.util.ArrayList; import java.util.List; -import java.util.Set; import java.util.concurrent.CompletableFuture; import java.util.function.BiConsumer; @@ -45,9 +43,8 @@ public CompletableFuture execute(ExecutionContext executionCont ExecutionStrategyInstrumentationContext executionStrategyCtx = ExecutionStrategyInstrumentationContext.nonNullCtx(instrumentation.beginExecutionStrategy(instrumentationParameters, executionContext.getInstrumentationState())); MergedSelectionSet fields = parameters.getFields(); - Set fieldNames = fields.keySet(); + List fieldNames = fields.getKeys(); Async.CombinedBuilder futures = Async.ofExpectedSize(fields.size()); - List resolvedFields = new ArrayList<>(fieldNames.size()); for (String fieldName : fieldNames) { MergedField currentField = fields.getSubField(fieldName); @@ -55,7 +52,6 @@ public CompletableFuture execute(ExecutionContext executionCont ExecutionStrategyParameters newParameters = parameters .transform(builder -> builder.field(currentField).path(fieldPath).parent(parameters)); - resolvedFields.add(fieldName); CompletableFuture future = resolveFieldWithInfo(executionContext, newParameters); futures.add(future); } @@ -63,7 +59,7 @@ public CompletableFuture execute(ExecutionContext executionCont executionStrategyCtx.onDispatched(overallResult); futures.await().whenComplete((completeValueInfos, throwable) -> { - BiConsumer, Throwable> handleResultsConsumer = handleResults(executionContext, resolvedFields, overallResult); + BiConsumer, Throwable> handleResultsConsumer = handleResults(executionContext, fieldNames, overallResult); if (throwable != null) { handleResultsConsumer.accept(null, throwable.getCause()); return; diff --git a/src/main/java/graphql/execution/ConditionalNodes.java b/src/main/java/graphql/execution/ConditionalNodes.java index 45b61986ba..a9e3ca733e 100644 --- a/src/main/java/graphql/execution/ConditionalNodes.java +++ b/src/main/java/graphql/execution/ConditionalNodes.java @@ -1,11 +1,13 @@ package graphql.execution; import graphql.Assert; +import graphql.GraphQLContext; import graphql.Internal; import graphql.language.Directive; import graphql.language.NodeUtil; import java.util.List; +import java.util.Locale; import java.util.Map; import static graphql.Directives.IncludeDirective; @@ -30,7 +32,7 @@ public boolean shouldInclude(Map variables, List dire private boolean getDirectiveResult(Map variables, List directives, String directiveName, boolean defaultValue) { Directive foundDirective = NodeUtil.findNodeByName(directives, directiveName); if (foundDirective != null) { - Map argumentValues = ValuesResolver.getArgumentValues(SkipDirective.getArguments(), foundDirective.getArguments(), CoercedVariables.of(variables)); + Map argumentValues = ValuesResolver.getArgumentValues(SkipDirective.getArguments(), foundDirective.getArguments(), CoercedVariables.of(variables), GraphQLContext.getDefault(), Locale.getDefault()); Object flag = argumentValues.get("if"); Assert.assertTrue(flag instanceof Boolean, () -> String.format("The '%s' directive MUST have a value for the 'if' argument", directiveName)); return (Boolean) flag; diff --git a/src/main/java/graphql/execution/DataFetcherExceptionHandler.java b/src/main/java/graphql/execution/DataFetcherExceptionHandler.java index 832b159429..a318ebf8df 100644 --- a/src/main/java/graphql/execution/DataFetcherExceptionHandler.java +++ b/src/main/java/graphql/execution/DataFetcherExceptionHandler.java @@ -1,12 +1,12 @@ package graphql.execution; +import graphql.DeprecatedAt; import graphql.ExecutionResult; import graphql.PublicSpi; import graphql.schema.DataFetcher; import graphql.schema.DataFetchingEnvironment; import java.util.concurrent.CompletableFuture; -import java.util.concurrent.CompletionStage; /** * This is called when an exception is thrown during {@link graphql.schema.DataFetcher#get(DataFetchingEnvironment)} execution @@ -27,6 +27,7 @@ public interface DataFetcherExceptionHandler { * version */ @Deprecated + @DeprecatedAt("2021-06-23") default DataFetcherExceptionHandlerResult onException(DataFetcherExceptionHandlerParameters handlerParameters) { return SimpleDataFetcherExceptionHandler.defaultImpl.onException(handlerParameters); } diff --git a/src/main/java/graphql/execution/DataFetcherResult.java b/src/main/java/graphql/execution/DataFetcherResult.java index ab195f9456..9b78497ed9 100644 --- a/src/main/java/graphql/execution/DataFetcherResult.java +++ b/src/main/java/graphql/execution/DataFetcherResult.java @@ -1,6 +1,7 @@ package graphql.execution; import com.google.common.collect.ImmutableList; +import graphql.DeprecatedAt; import graphql.GraphQLError; import graphql.Internal; import graphql.PublicApi; @@ -41,6 +42,7 @@ public class DataFetcherResult { */ @Internal @Deprecated + @DeprecatedAt("2019-01-11") public DataFetcherResult(T data, List errors) { this(data, errors, null); } diff --git a/src/main/java/graphql/execution/Execution.java b/src/main/java/graphql/execution/Execution.java index e0255f40f8..e09f13ada7 100644 --- a/src/main/java/graphql/execution/Execution.java +++ b/src/main/java/graphql/execution/Execution.java @@ -18,6 +18,7 @@ import graphql.language.VariableDefinition; import graphql.schema.GraphQLObjectType; import graphql.schema.GraphQLSchema; +import graphql.schema.impl.SchemaUtil; import graphql.util.LogKit; import org.slf4j.Logger; @@ -26,14 +27,10 @@ import java.util.Map; import java.util.concurrent.CompletableFuture; -import static graphql.Assert.assertShouldNeverHappen; import static graphql.execution.ExecutionContextBuilder.newExecutionContextBuilder; import static graphql.execution.ExecutionStepInfo.newExecutionStepInfo; import static graphql.execution.ExecutionStrategyParameters.newParameters; import static graphql.execution.instrumentation.SimpleInstrumentationContext.nonNullCtx; -import static graphql.language.OperationDefinition.Operation.MUTATION; -import static graphql.language.OperationDefinition.Operation.QUERY; -import static graphql.language.OperationDefinition.Operation.SUBSCRIPTION; import static java.util.concurrent.CompletableFuture.completedFuture; @Internal @@ -66,7 +63,7 @@ public CompletableFuture execute(Document document, GraphQLSche CoercedVariables coercedVariables; try { - coercedVariables = ValuesResolver.coerceVariableValues(graphQLSchema, variableDefinitions, inputVariables); + coercedVariables = ValuesResolver.coerceVariableValues(graphQLSchema, variableDefinitions, inputVariables, executionInput.getGraphQLContext(), executionInput.getLocale()); } catch (RuntimeException rte) { if (rte instanceof GraphQLError) { return completedFuture(new ExecutionResultImpl((GraphQLError) rte)); @@ -115,7 +112,7 @@ private CompletableFuture executeOperation(ExecutionContext exe GraphQLObjectType operationRootType; try { - operationRootType = getOperationRootType(executionContext.getGraphQLSchema(), operationDefinition); + operationRootType = SchemaUtil.getOperationRootType(executionContext.getGraphQLSchema(), operationDefinition); } catch (RuntimeException rte) { if (rte instanceof GraphQLError) { ExecutionResult executionResult = new ExecutionResultImpl(Collections.singletonList((GraphQLError) rte)); @@ -158,12 +155,12 @@ private CompletableFuture executeOperation(ExecutionContext exe } result = executionStrategy.execute(executionContext, parameters); } catch (NonNullableFieldWasNullException e) { - // this means it was non null types all the way from an offending non null type - // up to the root object type and there was a a null value some where. + // this means it was non-null types all the way from an offending non-null type + // up to the root object type and there was a null value somewhere. // // The spec says we should return null for the data in this case // - // http://facebook.github.io/graphql/#sec-Errors-and-Non-Nullability + // https://spec.graphql.org/October2021/#sec-Handling-Field-Errors // result = completedFuture(new ExecutionResultImpl(null, executionContext.getErrors())); } @@ -175,30 +172,4 @@ private CompletableFuture executeOperation(ExecutionContext exe return result; } - - - private GraphQLObjectType getOperationRootType(GraphQLSchema graphQLSchema, OperationDefinition operationDefinition) { - OperationDefinition.Operation operation = operationDefinition.getOperation(); - if (operation == MUTATION) { - GraphQLObjectType mutationType = graphQLSchema.getMutationType(); - if (mutationType == null) { - throw new MissingRootTypeException("Schema is not configured for mutations.", operationDefinition.getSourceLocation()); - } - return mutationType; - } else if (operation == QUERY) { - GraphQLObjectType queryType = graphQLSchema.getQueryType(); - if (queryType == null) { - throw new MissingRootTypeException("Schema does not define the required query root type.", operationDefinition.getSourceLocation()); - } - return queryType; - } else if (operation == SUBSCRIPTION) { - GraphQLObjectType subscriptionType = graphQLSchema.getSubscriptionType(); - if (subscriptionType == null) { - throw new MissingRootTypeException("Schema is not configured for subscriptions.", operationDefinition.getSourceLocation()); - } - return subscriptionType; - } else { - return assertShouldNeverHappen("Unhandled case. An extra operation enum has been added without code support"); - } - } } diff --git a/src/main/java/graphql/execution/ExecutionContext.java b/src/main/java/graphql/execution/ExecutionContext.java index d69c2b8672..9c1aa2032b 100644 --- a/src/main/java/graphql/execution/ExecutionContext.java +++ b/src/main/java/graphql/execution/ExecutionContext.java @@ -3,6 +3,7 @@ import com.google.common.collect.ImmutableList; import com.google.common.collect.ImmutableMap; +import graphql.DeprecatedAt; import graphql.ExecutionInput; import graphql.GraphQLContext; import graphql.GraphQLError; @@ -121,6 +122,7 @@ public OperationDefinition getOperationDefinition() { * @deprecated use {@link #getCoercedVariables()} instead */ @Deprecated + @DeprecatedAt("2022-05-24") public Map getVariables() { return coercedVariables.toMap(); } @@ -136,6 +138,7 @@ public CoercedVariables getCoercedVariables() { * @deprecated use {@link #getGraphQLContext()} instead */ @Deprecated + @DeprecatedAt("2021-07-05") @SuppressWarnings({"unchecked", "TypeParameterUnusedInFormals"}) public T getContext() { return (T) context; @@ -164,6 +167,7 @@ public DataLoaderRegistry getDataLoaderRegistry() { } @Deprecated + @DeprecatedAt("2022-07-26") public CacheControl getCacheControl() { return cacheControl; } @@ -185,7 +189,7 @@ public ValueUnboxer getValueUnboxer() { public void addError(GraphQLError error, ResultPath fieldPath) { synchronized (this) { // - // see http://facebook.github.io/graphql/#sec-Errors-and-Non-Nullability about how per + // see https://spec.graphql.org/October2021/#sec-Handling-Field-Errors about how per // field errors should be handled - ie only once per field if it's already there for nullability // but unclear if it's not that error path // diff --git a/src/main/java/graphql/execution/ExecutionContextBuilder.java b/src/main/java/graphql/execution/ExecutionContextBuilder.java index 6aaabf485c..12198cf879 100644 --- a/src/main/java/graphql/execution/ExecutionContextBuilder.java +++ b/src/main/java/graphql/execution/ExecutionContextBuilder.java @@ -2,6 +2,7 @@ import com.google.common.collect.ImmutableList; import com.google.common.collect.ImmutableMap; +import graphql.DeprecatedAt; import graphql.ExecutionInput; import graphql.GraphQLContext; import graphql.GraphQLError; @@ -130,6 +131,11 @@ public ExecutionContextBuilder subscriptionStrategy(ExecutionStrategy subscripti return this; } + /* + * @deprecated use {@link #graphQLContext(GraphQLContext)} instead + */ + @Deprecated + @DeprecatedAt("2021-07-05") public ExecutionContextBuilder context(Object context) { this.context = context; return this; @@ -157,6 +163,7 @@ public ExecutionContextBuilder root(Object root) { * @deprecated use {@link #coercedVariables(CoercedVariables)} instead */ @Deprecated + @DeprecatedAt("2022-05-24") public ExecutionContextBuilder variables(Map variables) { this.coercedVariables = CoercedVariables.of(variables); return this; @@ -188,6 +195,7 @@ public ExecutionContextBuilder dataLoaderRegistry(DataLoaderRegistry dataLoaderR } @Deprecated + @DeprecatedAt("2022-07-26") public ExecutionContextBuilder cacheControl(CacheControl cacheControl) { this.cacheControl = cacheControl; return this; diff --git a/src/main/java/graphql/execution/ExecutionStepInfo.java b/src/main/java/graphql/execution/ExecutionStepInfo.java index 60b5d6301c..884ca79179 100644 --- a/src/main/java/graphql/execution/ExecutionStepInfo.java +++ b/src/main/java/graphql/execution/ExecutionStepInfo.java @@ -1,5 +1,6 @@ package graphql.execution; +import graphql.DeprecatedAt; import graphql.PublicApi; import graphql.collect.ImmutableMapWithNullValues; import graphql.schema.GraphQLFieldDefinition; @@ -84,6 +85,7 @@ private ExecutionStepInfo(Builder builder) { * @deprecated use {@link #getObjectType()} instead as it is named better */ @Deprecated + @DeprecatedAt("2022-02-03") public GraphQLObjectType getFieldContainer() { return fieldContainer; } diff --git a/src/main/java/graphql/execution/ExecutionStepInfoFactory.java b/src/main/java/graphql/execution/ExecutionStepInfoFactory.java index 9042c90659..3338961388 100644 --- a/src/main/java/graphql/execution/ExecutionStepInfoFactory.java +++ b/src/main/java/graphql/execution/ExecutionStepInfoFactory.java @@ -23,7 +23,13 @@ public ExecutionStepInfo newExecutionStepInfoForSubField(ExecutionContext execut GraphQLOutputType fieldType = fieldDefinition.getType(); List fieldArgs = mergedField.getArguments(); GraphQLCodeRegistry codeRegistry = executionContext.getGraphQLSchema().getCodeRegistry(); - Supplier> argumentValues = FpKit.intraThreadMemoize(() -> ValuesResolver.getArgumentValues(codeRegistry, fieldDefinition.getArguments(), fieldArgs, executionContext.getCoercedVariables())); + Supplier> argumentValuesSupplier = () -> ValuesResolver.getArgumentValues(codeRegistry, + fieldDefinition.getArguments(), + fieldArgs, + executionContext.getCoercedVariables(), + executionContext.getGraphQLContext(), + executionContext.getLocale()); + Supplier> argumentValues = FpKit.intraThreadMemoize(argumentValuesSupplier); ResultPath newPath = parentInfo.getPath().segment(mergedField.getResultKey()); diff --git a/src/main/java/graphql/execution/ExecutionStrategy.java b/src/main/java/graphql/execution/ExecutionStrategy.java index bf2de70f22..33d45f5af7 100644 --- a/src/main/java/graphql/execution/ExecutionStrategy.java +++ b/src/main/java/graphql/execution/ExecutionStrategy.java @@ -37,6 +37,7 @@ import graphql.schema.GraphQLScalarType; import graphql.schema.GraphQLSchema; import graphql.schema.GraphQLType; +import graphql.schema.LightDataFetcher; import graphql.util.FpKit; import graphql.util.LogKit; import org.slf4j.Logger; @@ -106,7 +107,7 @@ * and the other "completeXXX" methods. *

* The order of fields fetching and completion is up to the execution strategy. As the graphql specification - * http://facebook.github.io/graphql/#sec-Normal-and-Serial-Execution says: + * https://spec.graphql.org/October2021/#sec-Normal-and-Serial-Execution says: *

* Normally the executor can execute the entries in a grouped field set in whatever order it chooses (often in parallel). Because * the resolution of fields other than top-level mutation fields must always be side effect-free and idempotent, the @@ -237,64 +238,59 @@ protected CompletableFuture fetchField(ExecutionContext executionC MergedField field = parameters.getField(); GraphQLObjectType parentType = (GraphQLObjectType) parameters.getExecutionStepInfo().getUnwrappedNonNullType(); GraphQLFieldDefinition fieldDef = getFieldDef(executionContext.getGraphQLSchema(), parentType, field.getSingleField()); - GraphQLCodeRegistry codeRegistry = executionContext.getGraphQLSchema().getCodeRegistry(); - GraphQLOutputType fieldType = fieldDef.getType(); - // if the DF (like PropertyDataFetcher) does not use the arguments of execution step info then dont build any - Supplier executionStepInfo = FpKit.intraThreadMemoize( - () -> createExecutionStepInfo(executionContext, parameters, fieldDef, parentType)); - Supplier> argumentValues = () -> executionStepInfo.get().getArguments(); + // if the DF (like PropertyDataFetcher) does not use the arguments or execution step info then dont build any - Supplier normalizedFieldSupplier = getNormalizedField(executionContext, parameters, executionStepInfo); + Supplier dataFetchingEnvironment = FpKit.intraThreadMemoize(() -> { - // DataFetchingFieldSelectionSet and QueryDirectives is a supplier of sorts - eg a lazy pattern - DataFetchingFieldSelectionSet fieldCollector = DataFetchingFieldSelectionSetImpl.newCollector(executionContext.getGraphQLSchema(), fieldType, normalizedFieldSupplier); - QueryDirectives queryDirectives = new QueryDirectivesImpl(field, executionContext.getGraphQLSchema(), executionContext.getVariables()); + Supplier executionStepInfo = FpKit.intraThreadMemoize( + () -> createExecutionStepInfo(executionContext, parameters, fieldDef, parentType)); + Supplier> argumentValues = () -> executionStepInfo.get().getArguments(); - DataFetchingEnvironment environment = newDataFetchingEnvironment(executionContext) - .source(parameters.getSource()) - .localContext(parameters.getLocalContext()) - .arguments(argumentValues) - .fieldDefinition(fieldDef) - .mergedField(parameters.getField()) - .fieldType(fieldType) - .executionStepInfo(executionStepInfo) - .parentType(parentType) - .selectionSet(fieldCollector) - .queryDirectives(queryDirectives) - .build(); + Supplier normalizedFieldSupplier = getNormalizedField(executionContext, parameters, executionStepInfo); + + // DataFetchingFieldSelectionSet and QueryDirectives is a supplier of sorts - eg a lazy pattern + DataFetchingFieldSelectionSet fieldCollector = DataFetchingFieldSelectionSetImpl.newCollector(executionContext.getGraphQLSchema(), fieldDef.getType(), normalizedFieldSupplier); + QueryDirectives queryDirectives = new QueryDirectivesImpl(field, + executionContext.getGraphQLSchema(), + executionContext.getCoercedVariables().toMap(), + executionContext.getGraphQLContext(), + executionContext.getLocale()); + + return newDataFetchingEnvironment(executionContext) + .source(parameters.getSource()) + .localContext(parameters.getLocalContext()) + .arguments(argumentValues) + .fieldDefinition(fieldDef) + .mergedField(parameters.getField()) + .fieldType(fieldDef.getType()) + .executionStepInfo(executionStepInfo) + .parentType(parentType) + .selectionSet(fieldCollector) + .queryDirectives(queryDirectives) + .build(); + }); DataFetcher dataFetcher = codeRegistry.getDataFetcher(parentType, fieldDef); Instrumentation instrumentation = executionContext.getInstrumentation(); - InstrumentationFieldFetchParameters instrumentationFieldFetchParams = new InstrumentationFieldFetchParameters(executionContext, environment, parameters, dataFetcher instanceof TrivialDataFetcher); + InstrumentationFieldFetchParameters instrumentationFieldFetchParams = new InstrumentationFieldFetchParameters(executionContext, dataFetchingEnvironment, parameters, dataFetcher instanceof TrivialDataFetcher); InstrumentationContext fetchCtx = nonNullCtx(instrumentation.beginFieldFetch(instrumentationFieldFetchParams, executionContext.getInstrumentationState()) ); - CompletableFuture fetchedValue; dataFetcher = instrumentation.instrumentDataFetcher(dataFetcher, instrumentationFieldFetchParams, executionContext.getInstrumentationState()); - ExecutionId executionId = executionContext.getExecutionId(); - try { - Object fetchedValueRaw = dataFetcher.get(environment); - fetchedValue = Async.toCompletableFuture(fetchedValueRaw); - } catch (Exception e) { - if (logNotSafe.isDebugEnabled()) { - logNotSafe.debug(String.format("'%s', field '%s' fetch threw exception", executionId, executionStepInfo.get().getPath()), e); - } + CompletableFuture fetchedValue = invokeDataFetcher(executionContext, parameters, fieldDef, dataFetchingEnvironment, dataFetcher); - fetchedValue = new CompletableFuture<>(); - fetchedValue.completeExceptionally(e); - } fetchCtx.onDispatched(fetchedValue); return fetchedValue .handle((result, exception) -> { fetchCtx.onCompleted(result, exception); if (exception != null) { - return handleFetchingException(executionContext, environment, exception); + return handleFetchingException(executionContext, dataFetchingEnvironment.get(), exception); } else { return CompletableFuture.completedFuture(result); } @@ -303,6 +299,25 @@ protected CompletableFuture fetchField(ExecutionContext executionC .thenApply(result -> unboxPossibleDataFetcherResult(executionContext, parameters, result)); } + private CompletableFuture invokeDataFetcher(ExecutionContext executionContext, ExecutionStrategyParameters parameters, GraphQLFieldDefinition fieldDef, Supplier dataFetchingEnvironment, DataFetcher dataFetcher) { + CompletableFuture fetchedValue; + try { + Object fetchedValueRaw; + if (dataFetcher instanceof LightDataFetcher) { + fetchedValueRaw = ((LightDataFetcher) dataFetcher).get(fieldDef, parameters.getSource(), dataFetchingEnvironment); + } else { + fetchedValueRaw = dataFetcher.get(dataFetchingEnvironment.get()); + } + fetchedValue = Async.toCompletableFuture(fetchedValueRaw); + } catch (Exception e) { + if (logNotSafe.isDebugEnabled()) { + logNotSafe.debug(String.format("'%s', field '%s' fetch threw exception", executionContext.getExecutionId(), parameters.getPath()), e); + } + fetchedValue = Async.exceptionallyCompletedFuture(e); + } + return fetchedValue; + } + protected Supplier getNormalizedField(ExecutionContext executionContext, ExecutionStrategyParameters parameters, Supplier executionStepInfo) { Supplier normalizedQuery = executionContext.getNormalizedQueryTree(); return () -> normalizedQuery.get().getNormalizedField(parameters.getField(), executionStepInfo.get().getObjectType(), executionStepInfo.get().getPath()); @@ -345,21 +360,21 @@ protected CompletableFuture handleFetchingException(ExecutionContext exec .build(); try { - return asyncHandleException(dataFetcherExceptionHandler, handlerParameters, executionContext); + return asyncHandleException(dataFetcherExceptionHandler, handlerParameters); } catch (Exception handlerException) { handlerParameters = DataFetcherExceptionHandlerParameters.newExceptionParameters() .dataFetchingEnvironment(environment) .exception(handlerException) .build(); - return asyncHandleException(new SimpleDataFetcherExceptionHandler(), handlerParameters, executionContext); + return asyncHandleException(new SimpleDataFetcherExceptionHandler(), handlerParameters); } } - private CompletableFuture asyncHandleException(DataFetcherExceptionHandler handler, DataFetcherExceptionHandlerParameters handlerParameters, ExecutionContext executionContext) { + private CompletableFuture asyncHandleException(DataFetcherExceptionHandler handler, DataFetcherExceptionHandlerParameters handlerParameters) { //noinspection unchecked - return handler.handleException(handlerParameters) - .thenApply(handlerResult -> (T) DataFetcherResult.newResult().errors(handlerResult.getErrors()).build() - ); + return handler.handleException(handlerParameters).thenApply( + handlerResult -> (T) DataFetcherResult.newResult().errors(handlerResult.getErrors()).build() + ); } /** @@ -590,16 +605,11 @@ protected FieldValueInfo completeValueForList(ExecutionContext executionContext, protected CompletableFuture completeValueForScalar(ExecutionContext executionContext, ExecutionStrategyParameters parameters, GraphQLScalarType scalarType, Object result) { Object serialized; try { - serialized = scalarType.getCoercing().serialize(result); + serialized = scalarType.getCoercing().serialize(result, executionContext.getGraphQLContext(), executionContext.getLocale()); } catch (CoercingSerializeException e) { serialized = handleCoercionProblem(executionContext, parameters, e); } - // TODO: fix that: this should not be handled here - //6.6.1 http://facebook.github.io/graphql/#sec-Field-entries - if (serialized instanceof Double && ((Double) serialized).isNaN()) { - serialized = null; - } try { serialized = parameters.getNonNullFieldValidator().validate(parameters.getPath(), serialized); } catch (NonNullableFieldWasNullException e) { @@ -621,7 +631,7 @@ protected CompletableFuture completeValueForScalar(ExecutionCon protected CompletableFuture completeValueForEnum(ExecutionContext executionContext, ExecutionStrategyParameters parameters, GraphQLEnumType enumType, Object result) { Object serialized; try { - serialized = enumType.serialize(result); + serialized = enumType.serialize(result, executionContext.getGraphQLContext(), executionContext.getLocale()); } catch (CoercingSerializeException e) { serialized = handleCoercionProblem(executionContext, parameters, e); } @@ -634,7 +644,7 @@ protected CompletableFuture completeValueForEnum(ExecutionConte } /** - * Called to turn an java object value into an graphql object value + * Called to turn a java object value into an graphql object value * * @param executionContext contains the top level execution parameters * @param parameters contains the parameters holding the fields to be executed and source object @@ -650,7 +660,7 @@ protected CompletableFuture completeValueForObject(ExecutionCon .schema(executionContext.getGraphQLSchema()) .objectType(resolvedObjectType) .fragments(executionContext.getFragmentsByName()) - .variables(executionContext.getVariables()) + .variables(executionContext.getCoercedVariables().toMap()) .build(); MergedSelectionSet subFields = fieldCollector.collectFields(collectorParameters, parameters.getField()); @@ -680,7 +690,6 @@ private Object handleCoercionProblem(ExecutionContext context, ExecutionStrategy return null; } - /** * Converts an object that is known to should be an Iterable into one * @@ -695,6 +704,10 @@ protected Iterable toIterable(Object result) { } protected GraphQLObjectType resolveType(ExecutionContext executionContext, ExecutionStrategyParameters parameters, GraphQLType fieldType) { + // we can avoid a method call and type resolver environment allocation if we know it's an object type + if (fieldType instanceof GraphQLObjectType) { + return (GraphQLObjectType) fieldType; + } return resolvedType.resolveType(executionContext, parameters.getField(), parameters.getSource(), parameters.getExecutionStepInfo(), fieldType, parameters.getLocalContext()); } @@ -744,7 +757,7 @@ protected GraphQLFieldDefinition getFieldDef(GraphQLSchema schema, GraphQLObject } /** - * See (http://facebook.github.io/graphql/#sec-Errors-and-Non-Nullability), + * See (...), *

* If a non nullable child field type actually resolves to a null value and the parent type is nullable * then the parent must in fact become null @@ -820,7 +833,13 @@ protected ExecutionStepInfo createExecutionStepInfo(ExecutionContext executionCo if (!fieldArgDefs.isEmpty()) { List fieldArgs = field.getArguments(); GraphQLCodeRegistry codeRegistry = executionContext.getGraphQLSchema().getCodeRegistry(); - argumentValues = FpKit.intraThreadMemoize(() -> ValuesResolver.getArgumentValues(codeRegistry, fieldArgDefs, fieldArgs, executionContext.getCoercedVariables())); + Supplier> argValuesSupplier = () -> ValuesResolver.getArgumentValues(codeRegistry, + fieldArgDefs, + fieldArgs, + executionContext.getCoercedVariables(), + executionContext.getGraphQLContext(), + executionContext.getLocale()); + argumentValues = FpKit.intraThreadMemoize(argValuesSupplier); } diff --git a/src/main/java/graphql/execution/MergedField.java b/src/main/java/graphql/execution/MergedField.java index 4e69afa98b..2ce672bb5b 100644 --- a/src/main/java/graphql/execution/MergedField.java +++ b/src/main/java/graphql/execution/MergedField.java @@ -5,7 +5,6 @@ import graphql.language.Argument; import graphql.language.Field; -import java.util.ArrayList; import java.util.List; import java.util.Objects; import java.util.function.Consumer; @@ -13,7 +12,7 @@ import static graphql.Assert.assertNotEmpty; /** - * This represent all Fields in a query which overlap and are merged into one. + * This represents all Fields in a query which overlap and are merged into one. * This means they all represent the same field actually when the query is executed. * * Example query with more than one Field merged together: @@ -42,7 +41,7 @@ * } * * - * Here the me field is merged together including the sub selections. + * Here the field is merged together including the sub selections. * * A third example with different directives: *

@@ -55,7 +54,7 @@
  * 
* These examples make clear that you need to consider all merged fields together to have the full picture. * - * The actual logic when fields can successfully merged together is implemented in {#graphql.validation.rules.OverlappingFieldsCanBeMerged} + * The actual logic when fields can be successfully merged together is implemented in {#graphql.validation.rules.OverlappingFieldsCanBeMerged} */ @PublicApi public class MergedField { diff --git a/src/main/java/graphql/execution/MergedSelectionSet.java b/src/main/java/graphql/execution/MergedSelectionSet.java index 6c49b52cbd..321a82c7ec 100644 --- a/src/main/java/graphql/execution/MergedSelectionSet.java +++ b/src/main/java/graphql/execution/MergedSelectionSet.java @@ -1,10 +1,10 @@ package graphql.execution; import com.google.common.collect.ImmutableList; +import com.google.common.collect.ImmutableMap; import graphql.Assert; import graphql.PublicApi; -import java.util.LinkedHashMap; import java.util.List; import java.util.Map; import java.util.Set; @@ -13,10 +13,10 @@ @PublicApi public class MergedSelectionSet { - private final Map subFields; + private final ImmutableMap subFields; private MergedSelectionSet(Map subFields) { - this.subFields = Assert.assertNotNull(subFields); + this.subFields = ImmutableMap.copyOf(Assert.assertNotNull(subFields)); } public Map getSubFields() { @@ -52,7 +52,7 @@ public static Builder newMergedSelectionSet() { } public static class Builder { - private Map subFields = new LinkedHashMap<>(); + private Map subFields = ImmutableMap.of(); private Builder() { diff --git a/src/main/java/graphql/execution/NonNullableFieldValidator.java b/src/main/java/graphql/execution/NonNullableFieldValidator.java index 73f4f531af..af26b69008 100644 --- a/src/main/java/graphql/execution/NonNullableFieldValidator.java +++ b/src/main/java/graphql/execution/NonNullableFieldValidator.java @@ -4,10 +4,10 @@ import graphql.Internal; /** - * This will check that a value is non null when the type definition says it must be and it will throw {@link NonNullableFieldWasNullException} + * This will check that a value is non-null when the type definition says it must be and, it will throw {@link NonNullableFieldWasNullException} * if this is not the case. * - * See: http://facebook.github.io/graphql/#sec-Errors-and-Non-Nullability + * See: https://spec.graphql.org/October2021/#sec-Errors-and-Non-Nullability */ @Internal public class NonNullableFieldValidator { @@ -34,7 +34,7 @@ public NonNullableFieldValidator(ExecutionContext executionContext, ExecutionSte public T validate(ResultPath path, T result) throws NonNullableFieldWasNullException { if (result == null) { if (executionStepInfo.isNonNullType()) { - // see http://facebook.github.io/graphql/#sec-Errors-and-Non-Nullability + // see https://spec.graphql.org/October2021/#sec-Errors-and-Non-Nullability // // > If the field returns null because of an error which has already been added to the "errors" list in the response, // > the "errors" list must not be further affected. That is, only one error should be added to the errors list per field. diff --git a/src/main/java/graphql/execution/NonNullableFieldWasNullException.java b/src/main/java/graphql/execution/NonNullableFieldWasNullException.java index d21893ef2d..87324bb005 100644 --- a/src/main/java/graphql/execution/NonNullableFieldWasNullException.java +++ b/src/main/java/graphql/execution/NonNullableFieldWasNullException.java @@ -7,7 +7,7 @@ import static graphql.schema.GraphQLTypeUtil.simplePrint; /** - * See (http://facebook.github.io/graphql/#sec-Errors-and-Non-Nullability), but if a non nullable field + * See (https://spec.graphql.org/October2021/#sec-Errors-and-Non-Nullability), but if a non nullable field * actually resolves to a null value and the parent type is nullable then the parent must in fact become null * so we use exceptions to indicate this special case */ diff --git a/src/main/java/graphql/execution/ResolveType.java b/src/main/java/graphql/execution/ResolveType.java index fbc9b5f536..3ef9c556fb 100644 --- a/src/main/java/graphql/execution/ResolveType.java +++ b/src/main/java/graphql/execution/ResolveType.java @@ -1,5 +1,6 @@ package graphql.execution; +import graphql.Assert; import graphql.Internal; import graphql.TypeResolutionEnvironment; import graphql.normalized.ExecutableNormalizedField; @@ -19,9 +20,9 @@ @Internal public class ResolveType { - public GraphQLObjectType resolveType(ExecutionContext executionContext, MergedField field, Object source, ExecutionStepInfo executionStepInfo, GraphQLType fieldType, Object localContext) { - GraphQLObjectType resolvedType; + Assert.assertTrue(fieldType instanceof GraphQLInterfaceType || fieldType instanceof GraphQLUnionType, + () -> "The passed in fieldType MUST be an interface or union type : " + fieldType.getClass().getName()); DataFetchingFieldSelectionSet fieldSelectionSet = buildSelectionSet(executionContext, field, (GraphQLOutputType) fieldType, executionStepInfo); TypeResolutionEnvironment env = TypeResolutionParameters.newParameters() .field(field) @@ -35,13 +36,10 @@ public GraphQLObjectType resolveType(ExecutionContext executionContext, MergedFi .schema(executionContext.getGraphQLSchema()) .build(); if (fieldType instanceof GraphQLInterfaceType) { - resolvedType = resolveTypeForInterface(env, (GraphQLInterfaceType) fieldType); - } else if (fieldType instanceof GraphQLUnionType) { - resolvedType = resolveTypeForUnion(env, (GraphQLUnionType) fieldType); + return resolveTypeForInterface(env, (GraphQLInterfaceType) fieldType); } else { - resolvedType = (GraphQLObjectType) fieldType; + return resolveTypeForUnion(env, (GraphQLUnionType) fieldType); } - return resolvedType; } private DataFetchingFieldSelectionSet buildSelectionSet(ExecutionContext executionContext, MergedField field, GraphQLOutputType fieldType, ExecutionStepInfo executionStepInfo) { @@ -65,11 +63,9 @@ private GraphQLObjectType resolveAbstractType(TypeResolutionEnvironment env, Typ if (result == null) { throw new UnresolvedTypeException(abstractType); } - if (!env.getSchema().isPossibleType(abstractType, result)) { throw new UnresolvedTypeException(abstractType, result); } - return result; } diff --git a/src/main/java/graphql/execution/TypeResolutionParameters.java b/src/main/java/graphql/execution/TypeResolutionParameters.java index 0bb829cfb3..88a8e2db69 100644 --- a/src/main/java/graphql/execution/TypeResolutionParameters.java +++ b/src/main/java/graphql/execution/TypeResolutionParameters.java @@ -1,5 +1,6 @@ package graphql.execution; +import graphql.DeprecatedAt; import graphql.GraphQLContext; import graphql.Internal; import graphql.TypeResolutionEnvironment; @@ -74,6 +75,7 @@ public static Builder newParameters() { * @deprecated use {@link #getGraphQLContext()} instead */ @Deprecated + @DeprecatedAt("2021-07-05") public Object getContext() { return context; } @@ -124,6 +126,7 @@ public Builder schema(GraphQLSchema schema) { } @Deprecated + @DeprecatedAt("2021-07-05") public Builder context(Object context) { this.context = context; return this; diff --git a/src/main/java/graphql/execution/ValuesResolver.java b/src/main/java/graphql/execution/ValuesResolver.java index fb93b51ae4..78b2e79d83 100644 --- a/src/main/java/graphql/execution/ValuesResolver.java +++ b/src/main/java/graphql/execution/ValuesResolver.java @@ -1,22 +1,14 @@ package graphql.execution; -import com.google.common.collect.ImmutableList; -import graphql.AssertException; +import graphql.GraphQLContext; import graphql.Internal; -import graphql.Scalars; -import graphql.VisibleForTesting; import graphql.collect.ImmutableKit; import graphql.language.Argument; import graphql.language.ArrayValue; -import graphql.language.BooleanValue; -import graphql.language.EnumValue; -import graphql.language.FloatValue; -import graphql.language.IntValue; import graphql.language.NullValue; import graphql.language.ObjectField; import graphql.language.ObjectValue; -import graphql.language.StringValue; import graphql.language.Value; import graphql.language.VariableDefinition; import graphql.language.VariableReference; @@ -26,45 +18,33 @@ import graphql.schema.GraphQLArgument; import graphql.schema.GraphQLCodeRegistry; import graphql.schema.GraphQLEnumType; -import graphql.schema.GraphQLInputObjectField; import graphql.schema.GraphQLInputObjectType; import graphql.schema.GraphQLInputType; import graphql.schema.GraphQLList; -import graphql.schema.GraphQLNonNull; import graphql.schema.GraphQLScalarType; import graphql.schema.GraphQLSchema; import graphql.schema.GraphQLType; -import graphql.schema.GraphQLTypeUtil; import graphql.schema.InputValueWithState; -import graphql.schema.PropertyDataFetcherHelper; -import graphql.schema.visibility.DefaultGraphqlFieldVisibility; import graphql.schema.visibility.GraphqlFieldVisibility; -import graphql.util.FpKit; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; -import java.math.BigDecimal; -import java.math.BigInteger; import java.util.ArrayList; import java.util.Collections; import java.util.LinkedHashMap; import java.util.List; +import java.util.Locale; import java.util.Map; import static graphql.Assert.assertShouldNeverHappen; import static graphql.Assert.assertTrue; -import static graphql.collect.ImmutableKit.emptyList; -import static graphql.collect.ImmutableKit.map; import static graphql.execution.ValuesResolver.ValueMode.NORMALIZED; -import static graphql.language.NullValue.newNullValue; -import static graphql.language.ObjectField.newObjectField; +import static graphql.execution.ValuesResolverConversion.externalValueToInternalValueImpl; import static graphql.schema.GraphQLTypeUtil.isList; import static graphql.schema.GraphQLTypeUtil.isNonNull; import static graphql.schema.GraphQLTypeUtil.simplePrint; -import static graphql.schema.GraphQLTypeUtil.unwrapNonNull; import static graphql.schema.GraphQLTypeUtil.unwrapOne; import static graphql.schema.visibility.DefaultGraphqlFieldVisibility.DEFAULT_FIELD_VISIBILITY; -import static java.util.stream.Collectors.toList; @SuppressWarnings("rawtypes") @Internal @@ -87,28 +67,37 @@ public enum ValueMode { * @param schema the schema * @param variableDefinitions the variable definitions * @param rawVariables the supplied variables + * @param graphqlContext the GraphqlContext to use + * @param locale the Locale to use * * @return coerced variable values as a map */ public static CoercedVariables coerceVariableValues(GraphQLSchema schema, - List variableDefinitions, - RawVariables rawVariables) throws CoercingParseValueException, NonNullableValueCoercedAsNullException { + List variableDefinitions, + RawVariables rawVariables, + GraphQLContext graphqlContext, + Locale locale) throws CoercingParseValueException, NonNullableValueCoercedAsNullException { - return externalValueToInternalValueForVariables(schema, variableDefinitions, rawVariables); + return ValuesResolverConversion.externalValueToInternalValueForVariables(schema, variableDefinitions, rawVariables, graphqlContext, locale); } + /** * Normalized variables values are Literals with type information. No validation here! * * @param schema the schema to use * @param variableDefinitions the list of variable definitions * @param rawVariables the raw variables + * @param graphqlContext the GraphqlContext to use + * @param locale the Locale to use * * @return a map of the normalised values */ public static Map getNormalizedVariableValues(GraphQLSchema schema, - List variableDefinitions, - RawVariables rawVariables) { + List variableDefinitions, + RawVariables rawVariables, + GraphQLContext graphqlContext, + Locale locale) { GraphqlFieldVisibility fieldVisibility = schema.getCodeRegistry().getFieldVisibility(); Map result = new LinkedHashMap<>(); for (VariableDefinition variableDefinition : variableDefinitions) { @@ -127,7 +116,7 @@ public static Map getNormalizedVariableValues(Grap if (value == null) { result.put(variableName, new NormalizedInputValue(simplePrint(variableType), null)); } else { - Object literal = externalValueToLiteral(fieldVisibility, value, (GraphQLInputType) variableType, NORMALIZED); + Object literal = ValuesResolverConversion.externalValueToLiteral(fieldVisibility, value, (GraphQLInputType) variableType, NORMALIZED, graphqlContext, locale); result.put(variableName, new NormalizedInputValue(simplePrint(variableType), literal)); } } @@ -137,19 +126,24 @@ public static Map getNormalizedVariableValues(Grap } + /** * This is not used for validation: the argument literals are all validated and the variables are validated (when coerced) * * @param argumentTypes the list of argument types * @param arguments the AST arguments * @param coercedVariables the coerced variables + * @param graphqlContext the GraphqlContext to use + * @param locale the Locale to use * * @return a map of named argument values */ public static Map getArgumentValues(List argumentTypes, - List arguments, - CoercedVariables coercedVariables) { - return getArgumentValuesImpl(DEFAULT_FIELD_VISIBILITY, argumentTypes, arguments, coercedVariables); + List arguments, + CoercedVariables coercedVariables, + GraphQLContext graphqlContext, + Locale locale) { + return getArgumentValuesImpl(DEFAULT_FIELD_VISIBILITY, argumentTypes, arguments, coercedVariables, graphqlContext, locale); } /** @@ -162,8 +156,8 @@ public static Map getArgumentValues(List argume * @return a map of named normalised values */ public static Map getNormalizedArgumentValues(List argumentTypes, - List arguments, - Map normalizedVariables) { + List arguments, + Map normalizedVariables) { if (argumentTypes.isEmpty()) { return ImmutableKit.emptyMap(); } @@ -190,47 +184,48 @@ public static Map getNormalizedArgumentValues(List } public static Map getArgumentValues(GraphQLCodeRegistry codeRegistry, - List argumentTypes, - List arguments, - CoercedVariables coercedVariables) { - return getArgumentValuesImpl(codeRegistry.getFieldVisibility(), argumentTypes, arguments, coercedVariables); - } - - public static Value valueToLiteral(InputValueWithState inputValueWithState, GraphQLType type) { - return valueToLiteral(DEFAULT_FIELD_VISIBILITY, inputValueWithState, type); + List argumentTypes, + List arguments, + CoercedVariables coercedVariables, + GraphQLContext graphqlContext, + Locale locale) { + return getArgumentValuesImpl(codeRegistry.getFieldVisibility(), argumentTypes, arguments, coercedVariables, graphqlContext, locale); } /** * Takes a value which can be in different states (internal, literal, external value) and converts into Literal - * + *

* This assumes the value is valid! * * @param fieldVisibility the field visibility to use * @param inputValueWithState the input value * @param type the type of input value + * @param graphqlContext the GraphqlContext to use + * @param locale the Locale to use * * @return a value converted to a literal */ - public static Value valueToLiteral(@NotNull GraphqlFieldVisibility fieldVisibility, @NotNull InputValueWithState inputValueWithState, @NotNull GraphQLType type) { - return (Value) valueToLiteral(fieldVisibility, inputValueWithState, type, ValueMode.LITERAL); + public static Value valueToLiteral(@NotNull GraphqlFieldVisibility fieldVisibility, + @NotNull InputValueWithState inputValueWithState, + @NotNull GraphQLType type, + GraphQLContext graphqlContext, + Locale locale) { + return (Value) ValuesResolverConversion.valueToLiteralImpl(fieldVisibility, inputValueWithState, type, ValueMode.LITERAL, graphqlContext, locale); } - private static Object valueToLiteral(GraphqlFieldVisibility fieldVisibility, InputValueWithState inputValueWithState, GraphQLType type, ValueMode valueMode) { - if (inputValueWithState.isInternal()) { - if (valueMode == NORMALIZED) { - return assertShouldNeverHappen("can't infer normalized structure"); - } - return valueToLiteralLegacy(inputValueWithState.getValue(), type); - } - if (inputValueWithState.isLiteral()) { - return inputValueWithState.getValue(); - } - if (inputValueWithState.isExternal()) { - return ValuesResolver.externalValueToLiteral(fieldVisibility, inputValueWithState.getValue(), (GraphQLInputType) type, valueMode); - } - return assertShouldNeverHappen("unexpected value state " + inputValueWithState); + public static Value valueToLiteral(@NotNull InputValueWithState inputValueWithState, + @NotNull GraphQLType type, + GraphQLContext graphqlContext, + Locale locale) { + return (Value) ValuesResolverConversion.valueToLiteralImpl(DEFAULT_FIELD_VISIBILITY, inputValueWithState, type, ValueMode.LITERAL, graphqlContext, locale); } + public static Object valueToInternalValue(InputValueWithState inputValueWithState, + GraphQLType type, + GraphQLContext graphqlContext, + Locale locale) throws CoercingParseValueException, CoercingParseLiteralException { + return ValuesResolverConversion.valueToInternalValueImpl(inputValueWithState, type, graphqlContext, locale); + } /** * Converts an external value to an internal value @@ -238,200 +233,39 @@ private static Object valueToLiteral(GraphqlFieldVisibility fieldVisibility, Inp * @param fieldVisibility the field visibility to use * @param externalValue the input external value * @param type the type of input value + * @param graphqlContext the GraphqlContext to use + * @param locale the Locale to use * * @return a value converted to an internal value */ - public static Object externalValueToInternalValue(GraphqlFieldVisibility fieldVisibility, Object externalValue, GraphQLInputType type) { - return ValuesResolver.externalValueToInternalValue(fieldVisibility, type, externalValue); - } - - public static Object valueToInternalValue(InputValueWithState inputValueWithState, GraphQLType type) throws CoercingParseValueException, CoercingParseLiteralException { - DefaultGraphqlFieldVisibility fieldVisibility = DEFAULT_FIELD_VISIBILITY; - if (inputValueWithState.isInternal()) { - return inputValueWithState.getValue(); - } - if (inputValueWithState.isLiteral()) { - return ValuesResolver.literalToInternalValue(fieldVisibility, type, (Value) inputValueWithState.getValue(), CoercedVariables.emptyVariables()); - } - if (inputValueWithState.isExternal()) { - return ValuesResolver.externalValueToInternalValue(fieldVisibility, type, inputValueWithState.getValue()); - } - return assertShouldNeverHappen("unexpected value state " + inputValueWithState); + public static Object externalValueToInternalValue(GraphqlFieldVisibility fieldVisibility, + Object externalValue, + GraphQLInputType type, + GraphQLContext graphqlContext, + Locale locale) { + return externalValueToInternalValueImpl(fieldVisibility, type, externalValue, graphqlContext, locale); } @Nullable @SuppressWarnings("unchecked") - public static T getInputValueImpl(GraphQLInputType inputType, InputValueWithState inputValue) { + public static T getInputValueImpl(GraphQLInputType inputType, + InputValueWithState inputValue, + GraphQLContext graphqlContext, + Locale locale) { if (inputValue.isNotSet()) { return null; } - return (T) valueToInternalValue(inputValue, inputType); - } - - /** - * No validation: the external value is assumed to be valid. - */ - private static Object externalValueToLiteral(GraphqlFieldVisibility fieldVisibility, - @Nullable Object value, - GraphQLInputType type, - ValueMode valueMode - ) { - if (value == null) { - return newNullValue().build(); - } - if (GraphQLTypeUtil.isNonNull(type)) { - return externalValueToLiteral(fieldVisibility, value, (GraphQLInputType) unwrapNonNull(type), valueMode); - } - if (type instanceof GraphQLScalarType) { - return externalValueToLiteralForScalar((GraphQLScalarType) type, value); - } else if (type instanceof GraphQLEnumType) { - return externalValueToLiteralForEnum((GraphQLEnumType) type, value); - } else if (type instanceof GraphQLList) { - return externalValueToLiteralForList(fieldVisibility, (GraphQLList) type, value, valueMode); - } else if (type instanceof GraphQLInputObjectType) { - return externalValueToLiteralForObject(fieldVisibility, (GraphQLInputObjectType) type, value, valueMode); - } else { - return assertShouldNeverHappen("unexpected type %s", type); - } - } - - /** - * No validation - */ - private static Value externalValueToLiteralForScalar(GraphQLScalarType scalarType, Object value) { - return scalarType.getCoercing().valueToLiteral(value); - - } - - /** - * No validation - */ - private static Value externalValueToLiteralForEnum(GraphQLEnumType enumType, Object value) { - return enumType.valueToLiteral(value); - } - - /** - * No validation - */ - @SuppressWarnings("unchecked") - private static Object externalValueToLiteralForList(GraphqlFieldVisibility fieldVisibility, GraphQLList listType, Object value, ValueMode valueMode) { - GraphQLInputType wrappedType = (GraphQLInputType) listType.getWrappedType(); - List result = FpKit.toListOrSingletonList(value) - .stream() - .map(val -> externalValueToLiteral(fieldVisibility, val, wrappedType, valueMode)) - .collect(toList()); - if (valueMode == NORMALIZED) { - return result; - } else { - return ArrayValue.newArrayValue().values(result).build(); - } - } - - /** - * No validation - */ - @SuppressWarnings("unchecked") - private static Object externalValueToLiteralForObject(GraphqlFieldVisibility fieldVisibility, - GraphQLInputObjectType inputObjectType, - Object inputValue, - ValueMode valueMode) { - assertTrue(inputValue instanceof Map, () -> "Expect Map as input"); - Map inputMap = (Map) inputValue; - List fieldDefinitions = fieldVisibility.getFieldDefinitions(inputObjectType); - - Map normalizedResult = new LinkedHashMap<>(); - ImmutableList.Builder objectFields = ImmutableList.builder(); - for (GraphQLInputObjectField inputFieldDefinition : fieldDefinitions) { - GraphQLInputType fieldType = inputFieldDefinition.getType(); - String fieldName = inputFieldDefinition.getName(); - boolean hasValue = inputMap.containsKey(fieldName); - Object fieldValue = inputMap.getOrDefault(fieldName, null); - if (!hasValue && inputFieldDefinition.hasSetDefaultValue()) { - //TODO: consider valueMode - Object defaultValueLiteral = valueToLiteral(fieldVisibility, inputFieldDefinition.getInputFieldDefaultValue(), fieldType); - if (valueMode == ValueMode.LITERAL) { - normalizedResult.put(fieldName, new NormalizedInputValue(simplePrint(fieldType), defaultValueLiteral)); - } else { - objectFields.add(newObjectField().name(fieldName).value((Value) defaultValueLiteral).build()); - } - } else if (hasValue) { - if (fieldValue == null) { - if (valueMode == NORMALIZED) { - normalizedResult.put(fieldName, new NormalizedInputValue(simplePrint(fieldType), null)); - } else { - objectFields.add(newObjectField().name(fieldName).value(newNullValue().build()).build()); - } - } else { - Object literal = externalValueToLiteral(fieldVisibility, - fieldValue, - fieldType, - valueMode); - if (valueMode == NORMALIZED) { - normalizedResult.put(fieldName, new NormalizedInputValue(simplePrint(fieldType), literal)); - } else { - objectFields.add(newObjectField().name(fieldName).value((Value) literal).build()); - } - } - } - } - if (valueMode == NORMALIZED) { - return normalizedResult; - } - return ObjectValue.newObjectValue().objectFields(objectFields.build()).build(); - } - - - /** - * performs validation too - */ - private static CoercedVariables externalValueToInternalValueForVariables(GraphQLSchema schema, - List variableDefinitions, - RawVariables rawVariables) { - GraphqlFieldVisibility fieldVisibility = schema.getCodeRegistry().getFieldVisibility(); - Map coercedValues = new LinkedHashMap<>(); - for (VariableDefinition variableDefinition : variableDefinitions) { - try { - String variableName = variableDefinition.getName(); - GraphQLType variableType = TypeFromAST.getTypeFromAST(schema, variableDefinition.getType()); - assertTrue(variableType instanceof GraphQLInputType); - // can be NullValue - Value defaultValue = variableDefinition.getDefaultValue(); - boolean hasValue = rawVariables.containsKey(variableName); - Object value = rawVariables.get(variableName); - if (!hasValue && defaultValue != null) { - Object coercedDefaultValue = literalToInternalValue(fieldVisibility, variableType, defaultValue, CoercedVariables.emptyVariables()); - coercedValues.put(variableName, coercedDefaultValue); - } else if (isNonNull(variableType) && (!hasValue || value == null)) { - throw new NonNullableValueCoercedAsNullException(variableDefinition, variableType); - } else if (hasValue) { - if (value == null) { - coercedValues.put(variableName, null); - } else { - Object coercedValue = externalValueToInternalValue(fieldVisibility, variableType, value); - coercedValues.put(variableName, coercedValue); - } - } - } catch (CoercingParseValueException e) { - throw CoercingParseValueException.newCoercingParseValueException() - .message(String.format("Variable '%s' has an invalid value: %s", variableDefinition.getName(), e.getMessage())) - .extensions(e.getExtensions()) - .cause(e.getCause()) - .sourceLocation(variableDefinition.getSourceLocation()) - .build(); - } catch (NonNullableValueCoercedAsNullException e) { - throw new NonNullableValueCoercedAsNullException(variableDefinition, e.getMessage()); - } - } - - return CoercedVariables.of(coercedValues); + return (T) valueToInternalValue(inputValue, inputType, graphqlContext, locale); } private static Map getArgumentValuesImpl(GraphqlFieldVisibility fieldVisibility, - List argumentTypes, - List arguments, - CoercedVariables coercedVariables) { + List argumentTypes, + List arguments, + CoercedVariables coercedVariables, + GraphQLContext graphqlContext, + Locale locale) { if (argumentTypes.isEmpty()) { return ImmutableKit.emptyMap(); } @@ -454,20 +288,21 @@ private static Map getArgumentValuesImpl(GraphqlFieldVisibility value = argumentValue; } if (!hasValue && argumentDefinition.hasSetDefaultValue()) { - Object coercedDefaultValue = defaultValueToInternalValue( + Object coercedDefaultValue = ValuesResolverConversion.defaultValueToInternalValue( fieldVisibility, defaultValue, - argumentType); + argumentType, + graphqlContext, locale); coercedValues.put(argumentName, coercedDefaultValue); - } else if (isNonNull(argumentType) && (!hasValue || isNullValue(value))) { + } else if (isNonNull(argumentType) && (!hasValue || ValuesResolverConversion.isNullValue(value))) { throw new NonNullableValueCoercedAsNullException(argumentDefinition); } else if (hasValue) { - if (isNullValue(value)) { + if (ValuesResolverConversion.isNullValue(value)) { coercedValues.put(argumentName, value); } else if (argumentValue instanceof VariableReference) { coercedValues.put(argumentName, value); } else { - value = literalToInternalValue(fieldVisibility, argumentType, argument.getValue(), coercedVariables); + value = ValuesResolverConversion.literalToInternalValue(fieldVisibility, argumentType, argument.getValue(), coercedVariables, graphqlContext, locale); coercedValues.put(argumentName, value); } } @@ -485,124 +320,10 @@ private static Map argumentMap(List arguments) { } - /** - * Performs validation too - */ - @SuppressWarnings("unchecked") - private static Object externalValueToInternalValue(GraphqlFieldVisibility fieldVisibility, - GraphQLType graphQLType, - Object value) throws NonNullableValueCoercedAsNullException, CoercingParseValueException { - if (isNonNull(graphQLType)) { - Object returnValue = - externalValueToInternalValue(fieldVisibility, unwrapOne(graphQLType), value); - if (returnValue == null) { - throw new NonNullableValueCoercedAsNullException(graphQLType); - } - return returnValue; - } - - if (value == null) { - return null; - } - - if (graphQLType instanceof GraphQLScalarType) { - return externalValueToInternalValueForScalar((GraphQLScalarType) graphQLType, value); - } else if (graphQLType instanceof GraphQLEnumType) { - return externalValueToInternalValueForEnum((GraphQLEnumType) graphQLType, value); - } else if (graphQLType instanceof GraphQLList) { - return externalValueToInternalValueForList(fieldVisibility, (GraphQLList) graphQLType, value); - } else if (graphQLType instanceof GraphQLInputObjectType) { - if (value instanceof Map) { - return externalValueToInternalValueForObject(fieldVisibility, (GraphQLInputObjectType) graphQLType, (Map) value); - } else { - throw CoercingParseValueException.newCoercingParseValueException() - .message("Expected type 'Map' but was '" + value.getClass().getSimpleName() + - "'. Variables for input objects must be an instance of type 'Map'.") - .build(); - } - } else { - return assertShouldNeverHappen("unhandled type %s", graphQLType); - } - } - - /** - * performs validation - */ - private static Object externalValueToInternalValueForObject(GraphqlFieldVisibility fieldVisibility, - GraphQLInputObjectType inputObjectType, - Map inputMap - ) throws NonNullableValueCoercedAsNullException, CoercingParseValueException { - List fieldDefinitions = fieldVisibility.getFieldDefinitions(inputObjectType); - List fieldNames = map(fieldDefinitions, GraphQLInputObjectField::getName); - for (String providedFieldName : inputMap.keySet()) { - if (!fieldNames.contains(providedFieldName)) { - throw new InputMapDefinesTooManyFieldsException(inputObjectType, providedFieldName); - } - } - - Map coercedValues = new LinkedHashMap<>(); - - for (GraphQLInputObjectField inputFieldDefinition : fieldDefinitions) { - GraphQLInputType fieldType = inputFieldDefinition.getType(); - String fieldName = inputFieldDefinition.getName(); - InputValueWithState defaultValue = inputFieldDefinition.getInputFieldDefaultValue(); - boolean hasValue = inputMap.containsKey(fieldName); - Object value; - Object fieldValue = inputMap.getOrDefault(fieldName, null); - value = fieldValue; - if (!hasValue && inputFieldDefinition.hasSetDefaultValue()) { - Object coercedDefaultValue = defaultValueToInternalValue(fieldVisibility, - defaultValue, - fieldType); - coercedValues.put(fieldName, coercedDefaultValue); - } else if (isNonNull(fieldType) && (!hasValue || value == null)) { - throw new NonNullableValueCoercedAsNullException(fieldName, emptyList(), fieldType); - } else if (hasValue) { - if (value == null) { - coercedValues.put(fieldName, null); - } else { - value = externalValueToInternalValue(fieldVisibility, - fieldType, value); - coercedValues.put(fieldName, value); - } - } - } - return coercedValues; - } - - /** - * including validation - */ - private static Object externalValueToInternalValueForScalar(GraphQLScalarType graphQLScalarType, Object value) throws CoercingParseValueException { - return graphQLScalarType.getCoercing().parseValue(value); - } - - /** - * including validation - */ - private static Object externalValueToInternalValueForEnum(GraphQLEnumType graphQLEnumType, Object value) throws CoercingParseValueException { - return graphQLEnumType.parseValue(value); - } - - /** - * including validation - */ - private static List externalValueToInternalValueForList(GraphqlFieldVisibility fieldVisibility, - GraphQLList graphQLList, - Object value - ) throws CoercingParseValueException, NonNullableValueCoercedAsNullException { - - GraphQLType wrappedType = graphQLList.getWrappedType(); - return FpKit.toListOrSingletonList(value) - .stream() - .map(val -> externalValueToInternalValue(fieldVisibility, wrappedType, val)) - .collect(toList()); - } - public static Object literalToNormalizedValue(GraphqlFieldVisibility fieldVisibility, - GraphQLType type, - Value inputValue, - Map normalizedVariables + GraphQLType type, + Value inputValue, + Map normalizedVariables ) { if (inputValue instanceof VariableReference) { String varName = ((VariableReference) inputValue).getName(); @@ -631,9 +352,9 @@ public static Object literalToNormalizedValue(GraphqlFieldVisibility fieldVisibi } private static Object literalToNormalizedValueForInputObject(GraphqlFieldVisibility fieldVisibility, - GraphQLInputObjectType type, - ObjectValue inputObjectLiteral, - Map normalizedVariables) { + GraphQLInputObjectType type, + ObjectValue inputObjectLiteral, + Map normalizedVariables) { Map result = new LinkedHashMap<>(); for (ObjectField field : inputObjectLiteral.getObjectFields()) { @@ -650,9 +371,9 @@ private static Object literalToNormalizedValueForInputObject(GraphqlFieldVisibil } private static List literalToNormalizedValueForList(GraphqlFieldVisibility fieldVisibility, - GraphQLList type, - Value value, - Map normalizedVariables) { + GraphQLList type, + Value value, + Map normalizedVariables) { if (value instanceof ArrayValue) { List result = new ArrayList<>(); for (Value valueInArray : ((ArrayValue) value).getValues()) { @@ -665,288 +386,6 @@ private static List literalToNormalizedValueForList(GraphqlFieldVisibili } - /** - * No validation (it was checked before via ArgumentsOfCorrectType and VariableDefaultValuesOfCorrectType) - * - * @param fieldVisibility the field visibility - * @param type the type of the input value - * @param inputValue the AST literal to be changed - * @param coercedVariables the coerced variable values - * - * @return literal converted to an internal value - */ - public static Object literalToInternalValue(GraphqlFieldVisibility fieldVisibility, - GraphQLType type, - Value inputValue, - CoercedVariables coercedVariables) { - - if (inputValue instanceof VariableReference) { - return coercedVariables.get(((VariableReference) inputValue).getName()); - } - if (inputValue instanceof NullValue) { - return null; - } - if (type instanceof GraphQLScalarType) { - return literalToInternalValueForScalar(inputValue, (GraphQLScalarType) type, coercedVariables); - } - if (isNonNull(type)) { - return literalToInternalValue(fieldVisibility, unwrapOne(type), inputValue, coercedVariables); - } - if (type instanceof GraphQLInputObjectType) { - return literalToInternalValueForInputObject(fieldVisibility, (GraphQLInputObjectType) type, (ObjectValue) inputValue, coercedVariables); - } - if (type instanceof GraphQLEnumType) { - return ((GraphQLEnumType) type).parseLiteral(inputValue); - } - if (isList(type)) { - return literalToInternalValueForList(fieldVisibility, (GraphQLList) type, inputValue, coercedVariables); - } - return null; - } - - /** - * no validation - */ - private static Object literalToInternalValueForScalar(Value inputValue, GraphQLScalarType scalarType, CoercedVariables coercedVariables) { - // the CoercingParseLiteralException exception that could happen here has been validated earlier via ValidationUtil - return scalarType.getCoercing().parseLiteral(inputValue, coercedVariables.toMap()); - } - - /** - * no validation - */ - private static Object literalToInternalValueForList(GraphqlFieldVisibility fieldVisibility, - GraphQLList graphQLList, - Value value, - CoercedVariables coercedVariables) { - - if (value instanceof ArrayValue) { - ArrayValue arrayValue = (ArrayValue) value; - List result = new ArrayList<>(); - for (Value singleValue : arrayValue.getValues()) { - result.add(literalToInternalValue(fieldVisibility, graphQLList.getWrappedType(), singleValue, coercedVariables)); - } - return result; - } else { - return Collections.singletonList( - literalToInternalValue(fieldVisibility, - graphQLList.getWrappedType(), - value, - coercedVariables)); - } - } - - /** - * no validation - */ - private static Object literalToInternalValueForInputObject(GraphqlFieldVisibility fieldVisibility, - GraphQLInputObjectType type, - ObjectValue inputValue, - CoercedVariables coercedVariables) { - Map coercedValues = new LinkedHashMap<>(); - - Map inputFieldsByName = mapObjectValueFieldsByName(inputValue); - - - List inputFieldTypes = fieldVisibility.getFieldDefinitions(type); - for (GraphQLInputObjectField inputFieldDefinition : inputFieldTypes) { - GraphQLInputType fieldType = inputFieldDefinition.getType(); - String fieldName = inputFieldDefinition.getName(); - ObjectField field = inputFieldsByName.get(fieldName); - boolean hasValue = field != null; - Object value; - Value fieldValue = field != null ? field.getValue() : null; - if (fieldValue instanceof VariableReference) { - String variableName = ((VariableReference) fieldValue).getName(); - hasValue = coercedVariables.containsKey(variableName); - value = coercedVariables.get(variableName); - } else { - value = fieldValue; - } - if (!hasValue && inputFieldDefinition.hasSetDefaultValue()) { - Object coercedDefaultValue = defaultValueToInternalValue(fieldVisibility, - inputFieldDefinition.getInputFieldDefaultValue(), - fieldType); - coercedValues.put(fieldName, coercedDefaultValue); - } else if (isNonNull(fieldType) && (!hasValue || isNullValue(value))) { - return assertShouldNeverHappen("Should have been validated before"); - } else if (hasValue) { - if (isNullValue(value)) { - coercedValues.put(fieldName, value); - } else if (fieldValue instanceof VariableReference) { - coercedValues.put(fieldName, value); - } else { - value = literalToInternalValue(fieldVisibility, fieldType, fieldValue, coercedVariables); - coercedValues.put(fieldName, value); - } - } - } - return coercedValues; - } - - private static boolean isNullValue(Object value) { - if (value == null) { - return true; - } - if (!(value instanceof NormalizedInputValue)) { - return false; - } - return ((NormalizedInputValue) value).getValue() == null; - } - - private static Map mapObjectValueFieldsByName(ObjectValue inputValue) { - Map inputValueFieldsByName = new LinkedHashMap<>(); - for (ObjectField objectField : inputValue.getObjectFields()) { - inputValueFieldsByName.put(objectField.getName(), objectField); - } - return inputValueFieldsByName; - } - - private static Object defaultValueToInternalValue(GraphqlFieldVisibility fieldVisibility, - InputValueWithState defaultValue, - GraphQLInputType type - ) { - if (defaultValue.isInternal()) { - return defaultValue.getValue(); - } - if (defaultValue.isLiteral()) { - // default value literals can't reference variables, this is why the variables are empty - return literalToInternalValue(fieldVisibility, type, (Value) defaultValue.getValue(), CoercedVariables.emptyVariables()); - } - if (defaultValue.isExternal()) { - // performs validation too - return externalValueToInternalValue(fieldVisibility, type, defaultValue.getValue()); - } - return assertShouldNeverHappen(); - } - - - /* - * ======================LEGACY=======+TO BE REMOVED IN THE FUTURE =============== - */ - - /** - * Legacy logic to convert an arbitrary java object to an Ast Literal. - * Only provided here to preserve backwards compatibility. - */ - @VisibleForTesting - static Value valueToLiteralLegacy(Object value, GraphQLType type) { - assertTrue(!(value instanceof Value), () -> "Unexpected literal " + value); - if (value == null) { - return null; - } - - if (isNonNull(type)) { - return handleNonNullLegacy(value, (GraphQLNonNull) type); - } - - // Convert JavaScript array to GraphQL list. If the GraphQLType is a list, but - // the value is not an array, convert the value using the list's item type. - if (isList(type)) { - return handleListLegacy(value, (GraphQLList) type); - } - - // Populate the fields of the input object by creating ASTs from each value - // in the JavaScript object according to the fields in the input type. - if (type instanceof GraphQLInputObjectType) { - return handleInputObjectLegacy(value, (GraphQLInputObjectType) type); - } - - if (!(type instanceof GraphQLScalarType || type instanceof GraphQLEnumType)) { - throw new AssertException("Must provide Input Type, cannot use: " + type.getClass()); - } - - // Since value is an internally represented value, it must be serialized - // to an externally represented value before converting into an AST. - final Object serialized = serializeLegacy(type, value); - if (isNullishLegacy(serialized)) { - return null; - } - - // Others serialize based on their corresponding JavaScript scalar types. - if (serialized instanceof Boolean) { - return BooleanValue.newBooleanValue().value((Boolean) serialized).build(); - } - - String stringValue = serialized.toString(); - // numbers can be Int or Float values. - if (serialized instanceof Number) { - return handleNumberLegacy(stringValue); - } - - if (serialized instanceof String) { - // Enum types use Enum literals. - if (type instanceof GraphQLEnumType) { - return EnumValue.newEnumValue().name(stringValue).build(); - } - - // ID types can use Int literals. - if (type == Scalars.GraphQLID && stringValue.matches("^[0-9]+$")) { - return IntValue.newIntValue().value(new BigInteger(stringValue)).build(); - } - - return StringValue.newStringValue().value(stringValue).build(); - } - - throw new AssertException("'Cannot convert value to AST: " + serialized); - } - - private static Value handleInputObjectLegacy(Object javaValue, GraphQLInputObjectType type) { - List fields = type.getFields(); - List fieldNodes = new ArrayList<>(); - fields.forEach(field -> { - String fieldName = field.getName(); - GraphQLInputType fieldType = field.getType(); - Object fieldValueObj = PropertyDataFetcherHelper.getPropertyValue(fieldName, javaValue, fieldType); - Value nodeValue = valueToLiteralLegacy(fieldValueObj, fieldType); - if (nodeValue != null) { - fieldNodes.add(newObjectField().name(fieldName).value(nodeValue).build()); - } - }); - return ObjectValue.newObjectValue().objectFields(fieldNodes).build(); - } - - private static Value handleNumberLegacy(String stringValue) { - if (stringValue.matches("^[0-9]+$")) { - return IntValue.newIntValue().value(new BigInteger(stringValue)).build(); - } else { - return FloatValue.newFloatValue().value(new BigDecimal(stringValue)).build(); - } - } - - @SuppressWarnings("rawtypes") - private static Value handleListLegacy(Object value, GraphQLList type) { - GraphQLType itemType = type.getWrappedType(); - if (FpKit.isIterable(value)) { - List valuesNodes = FpKit.toListOrSingletonList(value) - .stream() - .map(item -> valueToLiteralLegacy(item, itemType)) - .collect(toList()); - return ArrayValue.newArrayValue().values(valuesNodes).build(); - } - return valueToLiteralLegacy(value, itemType); - } - - private static Value handleNonNullLegacy(Object _value, GraphQLNonNull type) { - GraphQLType wrappedType = type.getWrappedType(); - return valueToLiteralLegacy(_value, wrappedType); - } - - private static Object serializeLegacy(GraphQLType type, Object value) { - if (type instanceof GraphQLScalarType) { - return ((GraphQLScalarType) type).getCoercing().serialize(value); - } else { - return ((GraphQLEnumType) type).serialize(value); - } - } - - private static boolean isNullishLegacy(Object serialized) { - if (serialized instanceof Number) { - return Double.isNaN(((Number) serialized).doubleValue()); - } - return serialized == null; - } - /** * @return true if variable is absent from input, and if value is NOT a variable then false */ diff --git a/src/main/java/graphql/execution/ValuesResolverConversion.java b/src/main/java/graphql/execution/ValuesResolverConversion.java new file mode 100644 index 0000000000..9c32048dc9 --- /dev/null +++ b/src/main/java/graphql/execution/ValuesResolverConversion.java @@ -0,0 +1,568 @@ +package graphql.execution; + +import com.google.common.collect.ImmutableList; +import graphql.GraphQLContext; +import graphql.Internal; +import graphql.language.ArrayValue; +import graphql.language.NullValue; +import graphql.language.ObjectField; +import graphql.language.ObjectValue; +import graphql.language.Value; +import graphql.language.VariableDefinition; +import graphql.language.VariableReference; +import graphql.normalized.NormalizedInputValue; +import graphql.schema.CoercingParseValueException; +import graphql.schema.GraphQLEnumType; +import graphql.schema.GraphQLInputObjectField; +import graphql.schema.GraphQLInputObjectType; +import graphql.schema.GraphQLInputType; +import graphql.schema.GraphQLList; +import graphql.schema.GraphQLScalarType; +import graphql.schema.GraphQLSchema; +import graphql.schema.GraphQLType; +import graphql.schema.GraphQLTypeUtil; +import graphql.schema.InputValueWithState; +import graphql.schema.visibility.DefaultGraphqlFieldVisibility; +import graphql.schema.visibility.GraphqlFieldVisibility; +import graphql.util.FpKit; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Locale; +import java.util.Map; + +import static graphql.Assert.assertShouldNeverHappen; +import static graphql.Assert.assertTrue; +import static graphql.collect.ImmutableKit.emptyList; +import static graphql.collect.ImmutableKit.map; +import static graphql.execution.ValuesResolver.ValueMode.NORMALIZED; +import static graphql.language.NullValue.newNullValue; +import static graphql.language.ObjectField.newObjectField; +import static graphql.schema.GraphQLTypeUtil.isList; +import static graphql.schema.GraphQLTypeUtil.isNonNull; +import static graphql.schema.GraphQLTypeUtil.simplePrint; +import static graphql.schema.GraphQLTypeUtil.unwrapNonNull; +import static graphql.schema.GraphQLTypeUtil.unwrapOne; +import static graphql.schema.visibility.DefaultGraphqlFieldVisibility.DEFAULT_FIELD_VISIBILITY; +import static java.util.stream.Collectors.toList; + +/** + * This class, originally broken out from {@link ValuesResolver} contains code for the conversion of values + * from one form (literal, external etc..) to another. + */ +@SuppressWarnings("rawtypes") +@Internal +class ValuesResolverConversion { + + static Object valueToLiteralImpl(GraphqlFieldVisibility fieldVisibility, + InputValueWithState inputValueWithState, + GraphQLType type, + ValuesResolver.ValueMode valueMode, + GraphQLContext graphqlContext, + Locale locale) { + if (inputValueWithState.isInternal()) { + if (valueMode == NORMALIZED) { + return assertShouldNeverHappen("can't infer normalized structure"); + } + return ValuesResolverLegacy.valueToLiteralLegacy(inputValueWithState.getValue(), type, graphqlContext, locale); + } + if (inputValueWithState.isLiteral()) { + return inputValueWithState.getValue(); + } + if (inputValueWithState.isExternal()) { + return externalValueToLiteral(fieldVisibility, inputValueWithState.getValue(), (GraphQLInputType) type, valueMode, graphqlContext, locale); + } + return assertShouldNeverHappen("unexpected value state " + inputValueWithState); + } + + /** + * Converts an external value to an internal value + * + * @param fieldVisibility the field visibility to use + * @param externalValue the input external value + * @param type the type of input value + * @param graphqlContext the GraphqlContext to use + * @param locale the Locale to use + * + * @return a value converted to an internal value + */ + static Object externalValueToInternalValue(GraphqlFieldVisibility fieldVisibility, + Object externalValue, + GraphQLInputType type, + GraphQLContext graphqlContext, + Locale locale) { + return externalValueToInternalValueImpl(fieldVisibility, type, externalValue, graphqlContext, locale); + } + + @Nullable + static Object valueToInternalValueImpl(InputValueWithState inputValueWithState, GraphQLType type, GraphQLContext graphqlContext, Locale locale) { + DefaultGraphqlFieldVisibility fieldVisibility = DEFAULT_FIELD_VISIBILITY; + if (inputValueWithState.isInternal()) { + return inputValueWithState.getValue(); + } + if (inputValueWithState.isLiteral()) { + return literalToInternalValue(fieldVisibility, type, (Value) inputValueWithState.getValue(), CoercedVariables.emptyVariables(), graphqlContext, locale); + } + if (inputValueWithState.isExternal()) { + return externalValueToInternalValueImpl(fieldVisibility, type, inputValueWithState.getValue(), graphqlContext, locale); + } + return assertShouldNeverHappen("unexpected value state " + inputValueWithState); + } + + /** + * No validation: the external value is assumed to be valid. + */ + static Object externalValueToLiteral(GraphqlFieldVisibility fieldVisibility, + @Nullable Object value, + GraphQLInputType type, + ValuesResolver.ValueMode valueMode, + GraphQLContext graphqlContext, + Locale locale) { + if (value == null) { + return newNullValue().build(); + } + if (GraphQLTypeUtil.isNonNull(type)) { + return externalValueToLiteral(fieldVisibility, value, (GraphQLInputType) unwrapNonNull(type), valueMode, graphqlContext, locale); + } + if (type instanceof GraphQLScalarType) { + return externalValueToLiteralForScalar((GraphQLScalarType) type, value, graphqlContext, locale); + } else if (type instanceof GraphQLEnumType) { + return externalValueToLiteralForEnum((GraphQLEnumType) type, value, graphqlContext, locale); + } else if (type instanceof GraphQLList) { + return externalValueToLiteralForList(fieldVisibility, (GraphQLList) type, value, valueMode, graphqlContext, locale); + } else if (type instanceof GraphQLInputObjectType) { + return externalValueToLiteralForObject(fieldVisibility, (GraphQLInputObjectType) type, value, valueMode, graphqlContext, locale); + } else { + return assertShouldNeverHappen("unexpected type %s", type); + } + } + + /** + * No validation + */ + private static Value externalValueToLiteralForScalar(GraphQLScalarType scalarType, Object value, GraphQLContext graphqlContext, @NotNull Locale locale) { + return scalarType.getCoercing().valueToLiteral(value, graphqlContext, locale); + + } + + /** + * No validation + */ + private static Value externalValueToLiteralForEnum(GraphQLEnumType enumType, Object value, GraphQLContext graphqlContext, Locale locale) { + return enumType.valueToLiteral(value, graphqlContext, locale); + } + + /** + * No validation + */ + @SuppressWarnings("unchecked") + private static Object externalValueToLiteralForList(GraphqlFieldVisibility fieldVisibility, + GraphQLList listType, + Object value, + ValuesResolver.ValueMode valueMode, + GraphQLContext graphqlContext, + Locale locale) { + GraphQLInputType wrappedType = (GraphQLInputType) listType.getWrappedType(); + List result = FpKit.toListOrSingletonList(value) + .stream() + .map(val -> externalValueToLiteral(fieldVisibility, val, wrappedType, valueMode, graphqlContext, locale)) + .collect(toList()); + if (valueMode == NORMALIZED) { + return result; + } else { + return ArrayValue.newArrayValue().values((List) result).build(); + } + } + + /** + * No validation + */ + @SuppressWarnings("unchecked") + private static Object externalValueToLiteralForObject(GraphqlFieldVisibility fieldVisibility, + GraphQLInputObjectType inputObjectType, + Object inputValue, + ValuesResolver.ValueMode valueMode, + GraphQLContext graphqlContext, Locale locale) { + assertTrue(inputValue instanceof Map, () -> "Expect Map as input"); + Map inputMap = (Map) inputValue; + List fieldDefinitions = fieldVisibility.getFieldDefinitions(inputObjectType); + + Map normalizedResult = new LinkedHashMap<>(); + ImmutableList.Builder objectFields = ImmutableList.builder(); + for (GraphQLInputObjectField inputFieldDefinition : fieldDefinitions) { + GraphQLInputType fieldType = inputFieldDefinition.getType(); + String fieldName = inputFieldDefinition.getName(); + boolean hasValue = inputMap.containsKey(fieldName); + Object fieldValue = inputMap.getOrDefault(fieldName, null); + if (!hasValue && inputFieldDefinition.hasSetDefaultValue()) { + //TODO: consider valueMode + Object defaultValueLiteral = valueToLiteralImpl(fieldVisibility, inputFieldDefinition.getInputFieldDefaultValue(), fieldType, ValuesResolver.ValueMode.LITERAL, graphqlContext, locale); + if (valueMode == ValuesResolver.ValueMode.LITERAL) { + normalizedResult.put(fieldName, new NormalizedInputValue(simplePrint(fieldType), defaultValueLiteral)); + } else { + objectFields.add(newObjectField().name(fieldName).value((Value) defaultValueLiteral).build()); + } + } else if (hasValue) { + if (fieldValue == null) { + if (valueMode == NORMALIZED) { + normalizedResult.put(fieldName, new NormalizedInputValue(simplePrint(fieldType), null)); + } else { + objectFields.add(newObjectField().name(fieldName).value(newNullValue().build()).build()); + } + } else { + Object literal = externalValueToLiteral(fieldVisibility, + fieldValue, + fieldType, + valueMode, + graphqlContext, locale); + if (valueMode == NORMALIZED) { + normalizedResult.put(fieldName, new NormalizedInputValue(simplePrint(fieldType), literal)); + } else { + objectFields.add(newObjectField().name(fieldName).value((Value) literal).build()); + } + } + } + } + if (valueMode == NORMALIZED) { + return normalizedResult; + } + return ObjectValue.newObjectValue().objectFields(objectFields.build()).build(); + } + + /** + * performs validation too + */ + static CoercedVariables externalValueToInternalValueForVariables(GraphQLSchema schema, + List variableDefinitions, + RawVariables rawVariables, + GraphQLContext graphqlContext, Locale locale) { + GraphqlFieldVisibility fieldVisibility = schema.getCodeRegistry().getFieldVisibility(); + Map coercedValues = new LinkedHashMap<>(); + for (VariableDefinition variableDefinition : variableDefinitions) { + try { + String variableName = variableDefinition.getName(); + GraphQLType variableType = TypeFromAST.getTypeFromAST(schema, variableDefinition.getType()); + assertTrue(variableType instanceof GraphQLInputType); + // can be NullValue + Value defaultValue = variableDefinition.getDefaultValue(); + boolean hasValue = rawVariables.containsKey(variableName); + Object value = rawVariables.get(variableName); + if (!hasValue && defaultValue != null) { + Object coercedDefaultValue = literalToInternalValue(fieldVisibility, variableType, defaultValue, CoercedVariables.emptyVariables(), graphqlContext, locale); + coercedValues.put(variableName, coercedDefaultValue); + } else if (isNonNull(variableType) && (!hasValue || value == null)) { + throw new NonNullableValueCoercedAsNullException(variableDefinition, variableType); + } else if (hasValue) { + if (value == null) { + coercedValues.put(variableName, null); + } else { + Object coercedValue = externalValueToInternalValueImpl(fieldVisibility, variableType, value, graphqlContext, locale); + coercedValues.put(variableName, coercedValue); + } + } + } catch (CoercingParseValueException e) { + throw CoercingParseValueException.newCoercingParseValueException() + .message(String.format("Variable '%s' has an invalid value: %s", variableDefinition.getName(), e.getMessage())) + .extensions(e.getExtensions()) + .cause(e.getCause()) + .sourceLocation(variableDefinition.getSourceLocation()) + .build(); + } catch (NonNullableValueCoercedAsNullException e) { + throw new NonNullableValueCoercedAsNullException(variableDefinition, e.getMessage()); + } + } + + return CoercedVariables.of(coercedValues); + } + + /** + * Performs validation too + */ + @SuppressWarnings("unchecked") + static Object externalValueToInternalValueImpl(GraphqlFieldVisibility fieldVisibility, + GraphQLType graphQLType, + Object value, + GraphQLContext graphqlContext, + Locale locale) throws NonNullableValueCoercedAsNullException, CoercingParseValueException { + if (isNonNull(graphQLType)) { + Object returnValue = + externalValueToInternalValueImpl(fieldVisibility, unwrapOne(graphQLType), value, graphqlContext, locale); + if (returnValue == null) { + throw new NonNullableValueCoercedAsNullException(graphQLType); + } + return returnValue; + } + + if (value == null) { + return null; + } + + if (graphQLType instanceof GraphQLScalarType) { + return externalValueToInternalValueForScalar((GraphQLScalarType) graphQLType, value, graphqlContext, locale); + } else if (graphQLType instanceof GraphQLEnumType) { + return externalValueToInternalValueForEnum((GraphQLEnumType) graphQLType, value, graphqlContext, locale); + } else if (graphQLType instanceof GraphQLList) { + return externalValueToInternalValueForList(fieldVisibility, (GraphQLList) graphQLType, value, graphqlContext, locale); + } else if (graphQLType instanceof GraphQLInputObjectType) { + if (value instanceof Map) { + return externalValueToInternalValueForObject(fieldVisibility, (GraphQLInputObjectType) graphQLType, (Map) value, graphqlContext, locale); + } else { + throw CoercingParseValueException.newCoercingParseValueException() + .message("Expected type 'Map' but was '" + value.getClass().getSimpleName() + + "'. Variables for input objects must be an instance of type 'Map'.") + .build(); + } + } else { + return assertShouldNeverHappen("unhandled type %s", graphQLType); + } + } + + /** + * performs validation + */ + private static Object externalValueToInternalValueForObject(GraphqlFieldVisibility fieldVisibility, + GraphQLInputObjectType inputObjectType, + Map inputMap, + GraphQLContext graphqlContext, + Locale locale) throws NonNullableValueCoercedAsNullException, CoercingParseValueException { + List fieldDefinitions = fieldVisibility.getFieldDefinitions(inputObjectType); + List fieldNames = map(fieldDefinitions, GraphQLInputObjectField::getName); + for (String providedFieldName : inputMap.keySet()) { + if (!fieldNames.contains(providedFieldName)) { + throw new InputMapDefinesTooManyFieldsException(inputObjectType, providedFieldName); + } + } + + Map coercedValues = new LinkedHashMap<>(); + + for (GraphQLInputObjectField inputFieldDefinition : fieldDefinitions) { + GraphQLInputType fieldType = inputFieldDefinition.getType(); + String fieldName = inputFieldDefinition.getName(); + InputValueWithState defaultValue = inputFieldDefinition.getInputFieldDefaultValue(); + boolean hasValue = inputMap.containsKey(fieldName); + Object value = inputMap.getOrDefault(fieldName, null); + if (!hasValue && inputFieldDefinition.hasSetDefaultValue()) { + Object coercedDefaultValue = defaultValueToInternalValue(fieldVisibility, + defaultValue, + fieldType, graphqlContext, locale); + coercedValues.put(fieldName, coercedDefaultValue); + } else if (isNonNull(fieldType) && (!hasValue || value == null)) { + throw new NonNullableValueCoercedAsNullException(fieldName, emptyList(), fieldType); + } else if (hasValue) { + if (value == null) { + coercedValues.put(fieldName, null); + } else { + value = externalValueToInternalValueImpl(fieldVisibility, + fieldType, value, graphqlContext, locale); + coercedValues.put(fieldName, value); + } + } + } + return coercedValues; + } + + /** + * including validation + */ + private static Object externalValueToInternalValueForScalar(GraphQLScalarType graphQLScalarType, Object value, GraphQLContext graphqlContext, Locale locale) throws CoercingParseValueException { + return graphQLScalarType.getCoercing().parseValue(value, graphqlContext, locale); + } + + /** + * including validation + */ + private static Object externalValueToInternalValueForEnum(GraphQLEnumType graphQLEnumType, Object value, GraphQLContext graphqlContext, Locale locale) throws CoercingParseValueException { + return graphQLEnumType.parseValue(value, graphqlContext, locale); + } + + /** + * including validation + */ + private static List externalValueToInternalValueForList(GraphqlFieldVisibility fieldVisibility, + GraphQLList graphQLList, + Object value, + GraphQLContext graphqlContext, + Locale locale) throws CoercingParseValueException, NonNullableValueCoercedAsNullException { + + GraphQLType wrappedType = graphQLList.getWrappedType(); + return FpKit.toListOrSingletonList(value) + .stream() + .map(val -> externalValueToInternalValueImpl(fieldVisibility, wrappedType, val, graphqlContext, locale)) + .collect(toList()); + } + + /** + * No validation (it was checked before via ArgumentsOfCorrectType and VariableDefaultValuesOfCorrectType) + * + * @param fieldVisibility the field visibility + * @param type the type of the input value + * @param inputValue the AST literal to be changed + * @param coercedVariables the coerced variable values + * @param graphqlContext the GraphqlContext to use + * @param locale the Locale to use + * + * @return literal converted to an internal value + */ + static Object literalToInternalValue(GraphqlFieldVisibility fieldVisibility, + GraphQLType type, + Value inputValue, + CoercedVariables coercedVariables, + GraphQLContext graphqlContext, + Locale locale) { + + return literalToInternalValueImpl(fieldVisibility, type, inputValue, coercedVariables, graphqlContext, locale); + } + + @Nullable + private static Object literalToInternalValueImpl(GraphqlFieldVisibility fieldVisibility, GraphQLType type, Value inputValue, CoercedVariables coercedVariables, GraphQLContext graphqlContext, Locale locale) { + if (inputValue instanceof VariableReference) { + return coercedVariables.get(((VariableReference) inputValue).getName()); + } + if (inputValue instanceof NullValue) { + return null; + } + if (type instanceof GraphQLScalarType) { + return literalToInternalValueForScalar(inputValue, (GraphQLScalarType) type, coercedVariables, graphqlContext, locale); + } + if (isNonNull(type)) { + return literalToInternalValue(fieldVisibility, unwrapOne(type), inputValue, coercedVariables, graphqlContext, locale); + } + if (type instanceof GraphQLInputObjectType) { + return literalToInternalValueForInputObject(fieldVisibility, (GraphQLInputObjectType) type, (ObjectValue) inputValue, coercedVariables, graphqlContext, locale); + } + if (type instanceof GraphQLEnumType) { + return ((GraphQLEnumType) type).parseLiteral(inputValue, graphqlContext, locale); + } + if (isList(type)) { + return literalToInternalValueForList(fieldVisibility, (GraphQLList) type, inputValue, coercedVariables, graphqlContext, locale); + } + return null; + } + + /** + * no validation + */ + private static Object literalToInternalValueForScalar(Value inputValue, GraphQLScalarType scalarType, CoercedVariables coercedVariables, GraphQLContext graphqlContext, @NotNull Locale locale) { + // the CoercingParseLiteralException exception that could happen here has been validated earlier via ValidationUtil + return scalarType.getCoercing().parseLiteral(inputValue, coercedVariables, graphqlContext, locale); + } + + /** + * no validation + */ + private static Object literalToInternalValueForList(GraphqlFieldVisibility fieldVisibility, + GraphQLList graphQLList, + Value value, + CoercedVariables coercedVariables, + GraphQLContext graphqlContext, + Locale locale) { + + if (value instanceof ArrayValue) { + ArrayValue arrayValue = (ArrayValue) value; + List result = new ArrayList<>(); + for (Value singleValue : arrayValue.getValues()) { + result.add(literalToInternalValue(fieldVisibility, graphQLList.getWrappedType(), singleValue, coercedVariables, graphqlContext, locale)); + } + return result; + } else { + return Collections.singletonList( + literalToInternalValue(fieldVisibility, + graphQLList.getWrappedType(), + value, + coercedVariables, + graphqlContext, locale)); + } + } + + /** + * no validation + */ + private static Object literalToInternalValueForInputObject(GraphqlFieldVisibility fieldVisibility, + GraphQLInputObjectType type, + ObjectValue inputValue, + CoercedVariables coercedVariables, + GraphQLContext graphqlContext, + Locale locale) { + Map coercedValues = new LinkedHashMap<>(); + + Map inputFieldsByName = mapObjectValueFieldsByName(inputValue); + + + List inputFieldTypes = fieldVisibility.getFieldDefinitions(type); + for (GraphQLInputObjectField inputFieldDefinition : inputFieldTypes) { + GraphQLInputType fieldType = inputFieldDefinition.getType(); + String fieldName = inputFieldDefinition.getName(); + ObjectField field = inputFieldsByName.get(fieldName); + boolean hasValue = field != null; + Object value; + Value fieldValue = field != null ? field.getValue() : null; + if (fieldValue instanceof VariableReference) { + String variableName = ((VariableReference) fieldValue).getName(); + hasValue = coercedVariables.containsKey(variableName); + value = coercedVariables.get(variableName); + } else { + value = fieldValue; + } + if (!hasValue && inputFieldDefinition.hasSetDefaultValue()) { + Object coercedDefaultValue = defaultValueToInternalValue(fieldVisibility, + inputFieldDefinition.getInputFieldDefaultValue(), + fieldType, + graphqlContext, locale); + coercedValues.put(fieldName, coercedDefaultValue); + } else if (isNonNull(fieldType) && (!hasValue || isNullValue(value))) { + return assertShouldNeverHappen("Should have been validated before"); + } else if (hasValue) { + if (isNullValue(value)) { + coercedValues.put(fieldName, value); + } else if (fieldValue instanceof VariableReference) { + coercedValues.put(fieldName, value); + } else { + value = literalToInternalValue(fieldVisibility, fieldType, fieldValue, coercedVariables, graphqlContext, locale); + coercedValues.put(fieldName, value); + } + } + } + return coercedValues; + } + + static boolean isNullValue(Object value) { + if (value == null) { + return true; + } + if (!(value instanceof NormalizedInputValue)) { + return false; + } + return ((NormalizedInputValue) value).getValue() == null; + } + + private static Map mapObjectValueFieldsByName(ObjectValue inputValue) { + Map inputValueFieldsByName = new LinkedHashMap<>(); + for (ObjectField objectField : inputValue.getObjectFields()) { + inputValueFieldsByName.put(objectField.getName(), objectField); + } + return inputValueFieldsByName; + } + + static Object defaultValueToInternalValue(GraphqlFieldVisibility fieldVisibility, + InputValueWithState defaultValue, + GraphQLInputType type, + GraphQLContext graphqlContext, + Locale locale) { + if (defaultValue.isInternal()) { + return defaultValue.getValue(); + } + if (defaultValue.isLiteral()) { + // default value literals can't reference variables, this is why the variables are empty + return literalToInternalValue(fieldVisibility, type, (Value) defaultValue.getValue(), CoercedVariables.emptyVariables(), graphqlContext, locale); + } + if (defaultValue.isExternal()) { + // performs validation too + return externalValueToInternalValueImpl(fieldVisibility, type, defaultValue.getValue(), graphqlContext, locale); + } + return assertShouldNeverHappen(); + } +} diff --git a/src/main/java/graphql/execution/ValuesResolverLegacy.java b/src/main/java/graphql/execution/ValuesResolverLegacy.java new file mode 100644 index 0000000000..81bc80ccdc --- /dev/null +++ b/src/main/java/graphql/execution/ValuesResolverLegacy.java @@ -0,0 +1,157 @@ +package graphql.execution; + +import graphql.AssertException; +import graphql.GraphQLContext; +import graphql.Internal; +import graphql.Scalars; +import graphql.VisibleForTesting; +import graphql.language.ArrayValue; +import graphql.language.BooleanValue; +import graphql.language.EnumValue; +import graphql.language.FloatValue; +import graphql.language.IntValue; +import graphql.language.ObjectField; +import graphql.language.ObjectValue; +import graphql.language.StringValue; +import graphql.language.Value; +import graphql.schema.GraphQLEnumType; +import graphql.schema.GraphQLInputObjectField; +import graphql.schema.GraphQLInputObjectType; +import graphql.schema.GraphQLInputType; +import graphql.schema.GraphQLList; +import graphql.schema.GraphQLNonNull; +import graphql.schema.GraphQLScalarType; +import graphql.schema.GraphQLType; +import graphql.schema.PropertyDataFetcherHelper; +import graphql.util.FpKit; + +import java.math.BigDecimal; +import java.math.BigInteger; +import java.util.ArrayList; +import java.util.List; +import java.util.Locale; + +import static graphql.Assert.assertTrue; +import static graphql.language.ObjectField.newObjectField; +import static graphql.schema.GraphQLTypeUtil.isList; +import static graphql.schema.GraphQLTypeUtil.isNonNull; +import static java.util.stream.Collectors.toList; + +/* + * ======================LEGACY=======+TO BE REMOVED IN THE FUTURE =============== + */ + +@Internal +class ValuesResolverLegacy { + /** + * Legacy logic to convert an arbitrary java object to an Ast Literal. + * Only provided here to preserve backwards compatibility. + */ + @VisibleForTesting + static Value valueToLiteralLegacy(Object value, GraphQLType type, GraphQLContext graphqlContext, Locale locale) { + assertTrue(!(value instanceof Value), () -> "Unexpected literal " + value); + if (value == null) { + return null; + } + + if (isNonNull(type)) { + return handleNonNullLegacy(value, (GraphQLNonNull) type, graphqlContext, locale); + } + + // Convert JavaScript array to GraphQL list. If the GraphQLType is a list, but + // the value is not an array, convert the value using the list's item type. + if (isList(type)) { + return handleListLegacy(value, (GraphQLList) type, graphqlContext, locale); + } + + // Populate the fields of the input object by creating ASTs from each value + // in the JavaScript object according to the fields in the input type. + if (type instanceof GraphQLInputObjectType) { + return handleInputObjectLegacy(value, (GraphQLInputObjectType) type, graphqlContext, locale); + } + + if (!(type instanceof GraphQLScalarType || type instanceof GraphQLEnumType)) { + throw new AssertException("Must provide Input Type, cannot use: " + type.getClass()); + } + + // Since value is an internally represented value, it must be serialized + // to an externally represented value before converting into an AST. + final Object serialized = serializeLegacy(type, value, graphqlContext, locale); + + // Others serialize based on their corresponding JavaScript scalar types. + if (serialized instanceof Boolean) { + return BooleanValue.newBooleanValue().value((Boolean) serialized).build(); + } + + String stringValue = serialized.toString(); + // numbers can be Int or Float values. + if (serialized instanceof Number) { + return handleNumberLegacy(stringValue); + } + + if (serialized instanceof String) { + // Enum types use Enum literals. + if (type instanceof GraphQLEnumType) { + return EnumValue.newEnumValue().name(stringValue).build(); + } + + // ID types can use Int literals. + if (type == Scalars.GraphQLID && stringValue.matches("^[0-9]+$")) { + return IntValue.newIntValue().value(new BigInteger(stringValue)).build(); + } + + return StringValue.newStringValue().value(stringValue).build(); + } + + throw new AssertException("'Cannot convert value to AST: " + serialized); + } + + private static Value handleInputObjectLegacy(Object javaValue, GraphQLInputObjectType type, GraphQLContext graphqlContext, Locale locale) { + List fields = type.getFields(); + List fieldNodes = new ArrayList<>(); + fields.forEach(field -> { + String fieldName = field.getName(); + GraphQLInputType fieldType = field.getType(); + Object fieldValueObj = PropertyDataFetcherHelper.getPropertyValue(fieldName, javaValue, fieldType); + Value nodeValue = valueToLiteralLegacy(fieldValueObj, fieldType, graphqlContext, locale); + if (nodeValue != null) { + fieldNodes.add(newObjectField().name(fieldName).value(nodeValue).build()); + } + }); + return ObjectValue.newObjectValue().objectFields(fieldNodes).build(); + } + + private static Value handleNumberLegacy(String stringValue) { + if (stringValue.matches("^[0-9]+$")) { + return IntValue.newIntValue().value(new BigInteger(stringValue)).build(); + } else { + return FloatValue.newFloatValue().value(new BigDecimal(stringValue)).build(); + } + } + + @SuppressWarnings("rawtypes") + private static Value handleListLegacy(Object value, GraphQLList type, GraphQLContext graphqlContext, Locale locale) { + GraphQLType itemType = type.getWrappedType(); + if (FpKit.isIterable(value)) { + List valuesNodes = FpKit.toListOrSingletonList(value) + .stream() + .map(item -> valueToLiteralLegacy(item, itemType, graphqlContext, locale)) + .collect(toList()); + return ArrayValue.newArrayValue().values(valuesNodes).build(); + } + return valueToLiteralLegacy(value, itemType, graphqlContext, locale); + } + + private static Value handleNonNullLegacy(Object _value, GraphQLNonNull type, GraphQLContext graphqlContext, Locale locale) { + GraphQLType wrappedType = type.getWrappedType(); + return valueToLiteralLegacy(_value, wrappedType, graphqlContext, locale); + } + + private static Object serializeLegacy(GraphQLType type, Object value, GraphQLContext graphqlContext, Locale locale) { + if (type instanceof GraphQLScalarType) { + return ((GraphQLScalarType) type).getCoercing().serialize(value, graphqlContext, locale); + } else { + return ((GraphQLEnumType) type).serialize(value, graphqlContext, locale); + } + } +} diff --git a/src/main/java/graphql/execution/directives/DirectivesResolver.java b/src/main/java/graphql/execution/directives/DirectivesResolver.java index 9c3d39a8c3..84722c3e40 100644 --- a/src/main/java/graphql/execution/directives/DirectivesResolver.java +++ b/src/main/java/graphql/execution/directives/DirectivesResolver.java @@ -1,6 +1,7 @@ package graphql.execution.directives; import com.google.common.collect.ImmutableMap; +import graphql.GraphQLContext; import graphql.Internal; import graphql.execution.CoercedVariables; import graphql.execution.ValuesResolver; @@ -12,6 +13,7 @@ import java.util.LinkedHashMap; import java.util.List; +import java.util.Locale; import java.util.Map; /** @@ -23,21 +25,27 @@ public class DirectivesResolver { public DirectivesResolver() { } - public Map resolveDirectives(List directives, GraphQLSchema schema, Map variables) { + public Map resolveDirectives(List directives, GraphQLSchema schema, Map variables, GraphQLContext graphQLContext, Locale locale) { GraphQLCodeRegistry codeRegistry = schema.getCodeRegistry(); Map directiveMap = new LinkedHashMap<>(); directives.forEach(directive -> { GraphQLDirective protoType = schema.getDirective(directive.getName()); if (protoType != null) { - GraphQLDirective newDirective = protoType.transform(builder -> buildArguments(builder, codeRegistry, protoType, directive, variables)); + GraphQLDirective newDirective = protoType.transform(builder -> buildArguments(builder, codeRegistry, protoType, directive, variables, graphQLContext, locale)); directiveMap.put(newDirective.getName(), newDirective); } }); return ImmutableMap.copyOf(directiveMap); } - private void buildArguments(GraphQLDirective.Builder directiveBuilder, GraphQLCodeRegistry codeRegistry, GraphQLDirective protoType, Directive fieldDirective, Map variables) { - Map argumentValues = ValuesResolver.getArgumentValues(codeRegistry, protoType.getArguments(), fieldDirective.getArguments(), CoercedVariables.of(variables)); + private void buildArguments(GraphQLDirective.Builder directiveBuilder, + GraphQLCodeRegistry codeRegistry, + GraphQLDirective protoType, + Directive fieldDirective, + Map variables, + GraphQLContext graphQLContext, + Locale locale) { + Map argumentValues = ValuesResolver.getArgumentValues(codeRegistry, protoType.getArguments(), fieldDirective.getArguments(), CoercedVariables.of(variables), graphQLContext, locale); directiveBuilder.clearArguments(); protoType.getArguments().forEach(protoArg -> { if (argumentValues.containsKey(protoArg.getName())) { diff --git a/src/main/java/graphql/execution/directives/QueryAppliedDirectiveArgument.java b/src/main/java/graphql/execution/directives/QueryAppliedDirectiveArgument.java index 40cb79c289..5deb4205b1 100644 --- a/src/main/java/graphql/execution/directives/QueryAppliedDirectiveArgument.java +++ b/src/main/java/graphql/execution/directives/QueryAppliedDirectiveArgument.java @@ -2,6 +2,7 @@ import graphql.Assert; +import graphql.GraphQLContext; import graphql.PublicApi; import graphql.language.Argument; import graphql.language.Value; @@ -12,6 +13,7 @@ import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; +import java.util.Locale; import java.util.function.Consumer; import static graphql.Assert.assertNotNull; @@ -83,7 +85,7 @@ public boolean hasSetValue() { */ @Nullable public T getValue() { - return getInputValueImpl(getType(), value); + return getInputValueImpl(getType(), value, GraphQLContext.getDefault(), Locale.getDefault()); } /** diff --git a/src/main/java/graphql/execution/directives/QueryDirectives.java b/src/main/java/graphql/execution/directives/QueryDirectives.java index 86cfb0d97d..97142f2069 100644 --- a/src/main/java/graphql/execution/directives/QueryDirectives.java +++ b/src/main/java/graphql/execution/directives/QueryDirectives.java @@ -1,5 +1,6 @@ package graphql.execution.directives; +import graphql.DeprecatedAt; import graphql.PublicApi; import graphql.language.Field; import graphql.schema.GraphQLDirective; @@ -51,6 +52,7 @@ public interface QueryDirectives { * @deprecated - use the {@link QueryAppliedDirective} methods instead */ @Deprecated + @DeprecatedAt("2022-02-24") Map> getImmediateDirectivesByName(); /** @@ -73,6 +75,7 @@ public interface QueryDirectives { * @deprecated - use the {@link QueryAppliedDirective} methods instead */ @Deprecated + @DeprecatedAt("2022-02-24") List getImmediateDirective(String directiveName); /** @@ -84,5 +87,6 @@ public interface QueryDirectives { * @deprecated - use the {@link QueryAppliedDirective} methods instead */ @Deprecated + @DeprecatedAt("2022-02-24") Map> getImmediateDirectivesByField(); } diff --git a/src/main/java/graphql/execution/directives/QueryDirectivesImpl.java b/src/main/java/graphql/execution/directives/QueryDirectivesImpl.java index b409abb1b0..85468c3470 100644 --- a/src/main/java/graphql/execution/directives/QueryDirectivesImpl.java +++ b/src/main/java/graphql/execution/directives/QueryDirectivesImpl.java @@ -2,6 +2,7 @@ import com.google.common.collect.ImmutableList; import com.google.common.collect.ImmutableMap; +import graphql.GraphQLContext; import graphql.Internal; import graphql.collect.ImmutableKit; import graphql.execution.MergedField; @@ -14,6 +15,7 @@ import java.util.ArrayList; import java.util.LinkedHashMap; import java.util.List; +import java.util.Locale; import java.util.Map; import static graphql.collect.ImmutableKit.emptyList; @@ -30,15 +32,19 @@ public class QueryDirectivesImpl implements QueryDirectives { private final MergedField mergedField; private final GraphQLSchema schema; private final Map variables; + private final GraphQLContext graphQLContext; + private final Locale locale; private volatile ImmutableMap> fieldDirectivesByField; private volatile ImmutableMap> fieldDirectivesByName; private volatile ImmutableMap> fieldAppliedDirectivesByField; private volatile ImmutableMap> fieldAppliedDirectivesByName; - public QueryDirectivesImpl(MergedField mergedField, GraphQLSchema schema, Map variables) { + public QueryDirectivesImpl(MergedField mergedField, GraphQLSchema schema, Map variables, GraphQLContext graphQLContext, Locale locale) { this.mergedField = mergedField; this.schema = schema; this.variables = variables; + this.graphQLContext = graphQLContext; + this.locale = locale; } private void computeValuesLazily() { @@ -53,7 +59,7 @@ private void computeValuesLazily() { List directives = field.getDirectives(); ImmutableList resolvedDirectives = ImmutableList.copyOf( directivesResolver - .resolveDirectives(directives, schema, variables) + .resolveDirectives(directives, schema, variables, graphQLContext, locale) .values() ); byField.put(field, resolvedDirectives); diff --git a/src/main/java/graphql/execution/instrumentation/ChainedInstrumentation.java b/src/main/java/graphql/execution/instrumentation/ChainedInstrumentation.java index 81c5184720..f8fdef568d 100644 --- a/src/main/java/graphql/execution/instrumentation/ChainedInstrumentation.java +++ b/src/main/java/graphql/execution/instrumentation/ChainedInstrumentation.java @@ -27,6 +27,7 @@ import java.util.List; import java.util.Map; import java.util.concurrent.CompletableFuture; +import java.util.function.Function; import static graphql.Assert.assertNotNull; import static graphql.collect.ImmutableKit.mapAndDropNulls; @@ -39,6 +40,7 @@ * * @see graphql.execution.instrumentation.Instrumentation */ +@SuppressWarnings("deprecation") @PublicApi public class ChainedInstrumentation implements Instrumentation { @@ -66,12 +68,26 @@ protected InstrumentationState getSpecificState(Instrumentation instrumentation, return chainedInstrumentationState.getState(instrumentation); } + private InstrumentationContext chainedCtx(Function> mapper) { + // if we have zero or 1 instrumentations (and 1 is the most common), then we can avoid an object allocation + // of the ChainedInstrumentationContext since it won't be needed + if (instrumentations.isEmpty()) { + return SimpleInstrumentationContext.noOp(); + } + if (instrumentations.size() == 1) { + return mapper.apply(instrumentations.get(0)); + } + return new ChainedInstrumentationContext<>(mapAndDropNulls(instrumentations, mapper)); + } + + @Override public InstrumentationState createState(InstrumentationCreateStateParameters parameters) { return new ChainedInstrumentationState(instrumentations, parameters); } @Override + @NotNull public InstrumentationContext beginExecution(InstrumentationExecutionParameters parameters) { // these assert methods have been left in so that we truly never call these methods, either in production nor in tests // later when the deprecated methods are removed, this will disappear. @@ -80,131 +96,148 @@ public InstrumentationContext beginExecution(InstrumentationExe @Override public InstrumentationContext beginExecution(InstrumentationExecutionParameters parameters, InstrumentationState state) { - return new ChainedInstrumentationContext<>(mapAndDropNulls(instrumentations, instrumentation -> { + return chainedCtx(instrumentation -> { InstrumentationState specificState = getSpecificState(instrumentation, state); return instrumentation.beginExecution(parameters, specificState); - })); + }); } @Override + @NotNull public InstrumentationContext beginParse(InstrumentationExecutionParameters parameters) { return Assert.assertShouldNeverHappen("The deprecated " + "beginParse" + " was called"); } @Override public InstrumentationContext beginParse(InstrumentationExecutionParameters parameters, InstrumentationState state) { - return new ChainedInstrumentationContext<>(mapAndDropNulls(instrumentations, instrumentation -> { + return chainedCtx(instrumentation -> { InstrumentationState specificState = getSpecificState(instrumentation, state); return instrumentation.beginParse(parameters, specificState); - })); + }); } @Override + @NotNull public InstrumentationContext> beginValidation(InstrumentationValidationParameters parameters) { return Assert.assertShouldNeverHappen("The deprecated " + "beginValidation" + " was called"); } @Override public InstrumentationContext> beginValidation(InstrumentationValidationParameters parameters, InstrumentationState state) { - return new ChainedInstrumentationContext<>(mapAndDropNulls(instrumentations, instrumentation -> { + return chainedCtx(instrumentation -> { InstrumentationState specificState = getSpecificState(instrumentation, state); return instrumentation.beginValidation(parameters, specificState); - })); + }); } @Override + @NotNull public InstrumentationContext beginExecuteOperation(InstrumentationExecuteOperationParameters parameters) { return Assert.assertShouldNeverHappen("The deprecated " + "beginExecuteOperation" + " was called"); } @Override public InstrumentationContext beginExecuteOperation(InstrumentationExecuteOperationParameters parameters, InstrumentationState state) { - return new ChainedInstrumentationContext<>(mapAndDropNulls(instrumentations, instrumentation -> { + return chainedCtx(instrumentation -> { InstrumentationState specificState = getSpecificState(instrumentation, state); return instrumentation.beginExecuteOperation(parameters, specificState); - })); - + }); } @Override + @NotNull public ExecutionStrategyInstrumentationContext beginExecutionStrategy(InstrumentationExecutionStrategyParameters parameters) { return Assert.assertShouldNeverHappen("The deprecated " + "beginExecutionStrategy" + " was called"); } @Override public ExecutionStrategyInstrumentationContext beginExecutionStrategy(InstrumentationExecutionStrategyParameters parameters, InstrumentationState state) { - return new ChainedExecutionStrategyInstrumentationContext(mapAndDropNulls(instrumentations, instrumentation -> { + if (instrumentations.isEmpty()) { + return ExecutionStrategyInstrumentationContext.NOOP; + } + Function mapper = instrumentation -> { InstrumentationState specificState = getSpecificState(instrumentation, state); return instrumentation.beginExecutionStrategy(parameters, specificState); - })); + }; + if (instrumentations.size() == 1) { + return mapper.apply(instrumentations.get(0)); + } + return new ChainedExecutionStrategyInstrumentationContext(mapAndDropNulls(instrumentations, mapper)); } @Override + @NotNull public InstrumentationContext beginSubscribedFieldEvent(InstrumentationFieldParameters parameters) { return Assert.assertShouldNeverHappen("The deprecated " + "beginSubscribedFieldEvent" + " was called"); } @Override public InstrumentationContext beginSubscribedFieldEvent(InstrumentationFieldParameters parameters, InstrumentationState state) { - return new ChainedInstrumentationContext<>(mapAndDropNulls(instrumentations, instrumentation -> { + return chainedCtx(instrumentation -> { InstrumentationState specificState = getSpecificState(instrumentation, state); return instrumentation.beginSubscribedFieldEvent(parameters, specificState); - })); + }); } @Override + @NotNull public InstrumentationContext beginField(InstrumentationFieldParameters parameters) { return Assert.assertShouldNeverHappen("The deprecated " + "beginField" + " was called"); } @Override public InstrumentationContext beginField(InstrumentationFieldParameters parameters, InstrumentationState state) { - return new ChainedInstrumentationContext<>(mapAndDropNulls(instrumentations, instrumentation -> { + return chainedCtx(instrumentation -> { InstrumentationState specificState = getSpecificState(instrumentation, state); return instrumentation.beginField(parameters, specificState); - })); + }); } @Override + @NotNull public InstrumentationContext beginFieldFetch(InstrumentationFieldFetchParameters parameters) { return Assert.assertShouldNeverHappen("The deprecated " + "beginFieldFetch" + " was called"); } @Override public InstrumentationContext beginFieldFetch(InstrumentationFieldFetchParameters parameters, InstrumentationState state) { - return new ChainedInstrumentationContext<>(mapAndDropNulls(instrumentations, instrumentation -> { + return chainedCtx(instrumentation -> { InstrumentationState specificState = getSpecificState(instrumentation, state); return instrumentation.beginFieldFetch(parameters, specificState); - })); + }); } + @Override + @NotNull public InstrumentationContext beginFieldComplete(InstrumentationFieldCompleteParameters parameters) { return Assert.assertShouldNeverHappen("The deprecated " + "beginFieldComplete" + " was called"); } @Override public InstrumentationContext beginFieldComplete(InstrumentationFieldCompleteParameters parameters, InstrumentationState state) { - return new ChainedInstrumentationContext<>(mapAndDropNulls(instrumentations, instrumentation -> { + return chainedCtx(instrumentation -> { InstrumentationState specificState = getSpecificState(instrumentation, state); return instrumentation.beginFieldComplete(parameters, specificState); - })); + }); } @Override + @NotNull public InstrumentationContext beginFieldListComplete(InstrumentationFieldCompleteParameters parameters) { return Assert.assertShouldNeverHappen("The deprecated " + "beginFieldListComplete" + " was called"); } @Override public InstrumentationContext beginFieldListComplete(InstrumentationFieldCompleteParameters parameters, InstrumentationState state) { - return new ChainedInstrumentationContext<>(mapAndDropNulls(instrumentations, it -> { - InstrumentationState specificState = getSpecificState(it, state); - return it.beginFieldListComplete(parameters, specificState); - })); + return chainedCtx(instrumentation -> { + InstrumentationState specificState = getSpecificState(instrumentation, state); + return instrumentation.beginFieldListComplete(parameters, specificState); + }); } @Override + @NotNull public ExecutionInput instrumentExecutionInput(ExecutionInput executionInput, InstrumentationExecutionParameters parameters) { return Assert.assertShouldNeverHappen("The deprecated " + "instrumentExecutionInput" + " was called"); } @@ -212,6 +245,9 @@ public ExecutionInput instrumentExecutionInput(ExecutionInput executionInput, In @NotNull @Override public ExecutionInput instrumentExecutionInput(ExecutionInput executionInput, InstrumentationExecutionParameters parameters, InstrumentationState state) { + if (instrumentations.isEmpty()) { + return executionInput; + } for (Instrumentation instrumentation : instrumentations) { InstrumentationState specificState = getSpecificState(instrumentation, state); executionInput = instrumentation.instrumentExecutionInput(executionInput, parameters, specificState); @@ -220,6 +256,7 @@ public ExecutionInput instrumentExecutionInput(ExecutionInput executionInput, In } @Override + @NotNull public DocumentAndVariables instrumentDocumentAndVariables(DocumentAndVariables documentAndVariables, InstrumentationExecutionParameters parameters) { return Assert.assertShouldNeverHappen("The deprecated " + "instrumentDocumentAndVariables" + " was called"); } @@ -227,6 +264,9 @@ public DocumentAndVariables instrumentDocumentAndVariables(DocumentAndVariables @NotNull @Override public DocumentAndVariables instrumentDocumentAndVariables(DocumentAndVariables documentAndVariables, InstrumentationExecutionParameters parameters, InstrumentationState state) { + if (instrumentations.isEmpty()) { + return documentAndVariables; + } for (Instrumentation instrumentation : instrumentations) { InstrumentationState specificState = getSpecificState(instrumentation, state); documentAndVariables = instrumentation.instrumentDocumentAndVariables(documentAndVariables, parameters, specificState); @@ -235,6 +275,7 @@ public DocumentAndVariables instrumentDocumentAndVariables(DocumentAndVariables } @Override + @NotNull public GraphQLSchema instrumentSchema(GraphQLSchema schema, InstrumentationExecutionParameters parameters) { return Assert.assertShouldNeverHappen("The deprecated " + "instrumentSchema" + " was called"); } @@ -242,6 +283,9 @@ public GraphQLSchema instrumentSchema(GraphQLSchema schema, InstrumentationExecu @NotNull @Override public GraphQLSchema instrumentSchema(GraphQLSchema schema, InstrumentationExecutionParameters parameters, InstrumentationState state) { + if (instrumentations.isEmpty()) { + return schema; + } for (Instrumentation instrumentation : instrumentations) { InstrumentationState specificState = getSpecificState(instrumentation, state); schema = instrumentation.instrumentSchema(schema, parameters, specificState); @@ -250,6 +294,7 @@ public GraphQLSchema instrumentSchema(GraphQLSchema schema, InstrumentationExecu } @Override + @NotNull public ExecutionContext instrumentExecutionContext(ExecutionContext executionContext, InstrumentationExecutionParameters parameters) { return Assert.assertShouldNeverHappen("The deprecated " + "instrumentExecutionContext" + " was called"); } @@ -257,6 +302,9 @@ public ExecutionContext instrumentExecutionContext(ExecutionContext executionCon @NotNull @Override public ExecutionContext instrumentExecutionContext(ExecutionContext executionContext, InstrumentationExecutionParameters parameters, InstrumentationState state) { + if (instrumentations.isEmpty()) { + return executionContext; + } for (Instrumentation instrumentation : instrumentations) { InstrumentationState specificState = getSpecificState(instrumentation, state); executionContext = instrumentation.instrumentExecutionContext(executionContext, parameters, specificState); @@ -265,6 +313,7 @@ public ExecutionContext instrumentExecutionContext(ExecutionContext executionCon } @Override + @NotNull public DataFetcher instrumentDataFetcher(DataFetcher dataFetcher, InstrumentationFieldFetchParameters parameters) { return Assert.assertShouldNeverHappen("The deprecated " + "instrumentDataFetcher" + " was called"); } @@ -272,6 +321,9 @@ public DataFetcher instrumentDataFetcher(DataFetcher dataFetcher, Instrume @NotNull @Override public DataFetcher instrumentDataFetcher(DataFetcher dataFetcher, InstrumentationFieldFetchParameters parameters, InstrumentationState state) { + if (instrumentations.isEmpty()) { + return dataFetcher; + } for (Instrumentation instrumentation : instrumentations) { InstrumentationState specificState = getSpecificState(instrumentation, state); dataFetcher = instrumentation.instrumentDataFetcher(dataFetcher, parameters, specificState); @@ -280,6 +332,7 @@ public DataFetcher instrumentDataFetcher(DataFetcher dataFetcher, Instrume } @Override + @NotNull public CompletableFuture instrumentExecutionResult(ExecutionResult executionResult, InstrumentationExecutionParameters parameters) { return Assert.assertShouldNeverHappen("The deprecated " + "instrumentExecutionResult" + " was called"); } diff --git a/src/main/java/graphql/execution/instrumentation/Instrumentation.java b/src/main/java/graphql/execution/instrumentation/Instrumentation.java index 1f19018284..989364c482 100644 --- a/src/main/java/graphql/execution/instrumentation/Instrumentation.java +++ b/src/main/java/graphql/execution/instrumentation/Instrumentation.java @@ -1,5 +1,6 @@ package graphql.execution.instrumentation; +import graphql.DeprecatedAt; import graphql.ExecutionInput; import graphql.ExecutionResult; import graphql.PublicSpi; @@ -26,13 +27,13 @@ /** * Provides the capability to instrument the execution steps of a GraphQL query. - * + *

* For example you might want to track which fields are taking the most time to fetch from the backing database * or log what fields are being asked for. - * + *

* Remember that graphql calls can cross threads so make sure you think about the thread safety of any instrumentation * code when you are writing it. - * + *

* Each step gives back an {@link graphql.execution.instrumentation.InstrumentationContext} object. This has two callbacks on it, * one for the step is `dispatched` and one for when the step has `completed`. This is done because many of the "steps" are asynchronous * operations such as fetching data and resolving it into objects. @@ -49,6 +50,7 @@ public interface Instrumentation { * @deprecated use {@link #createState(InstrumentationCreateStateParameters)} instead */ @Deprecated + @DeprecatedAt("2022-07-26") default InstrumentationState createState() { return null; } @@ -61,7 +63,7 @@ default InstrumentationState createState() { * * @return a state object that is passed to each method */ - @Nullable + @Nullable default InstrumentationState createState(InstrumentationCreateStateParameters parameters) { return createState(); } @@ -76,6 +78,7 @@ default InstrumentationState createState(InstrumentationCreateStateParameters pa * @deprecated use {@link #beginExecution(InstrumentationExecutionParameters, InstrumentationState)} instead */ @Deprecated + @DeprecatedAt("2022-07-26") @NotNull default InstrumentationContext beginExecution(InstrumentationExecutionParameters parameters) { return noOp(); @@ -104,6 +107,7 @@ default InstrumentationContext beginExecution(InstrumentationEx * @deprecated use {@link #beginParse(InstrumentationExecutionParameters, InstrumentationState)} instead */ @Deprecated + @DeprecatedAt("2022-07-26") @NotNull default InstrumentationContext beginParse(InstrumentationExecutionParameters parameters) { return noOp(); @@ -132,6 +136,7 @@ default InstrumentationContext beginParse(InstrumentationExecutionPara * @deprecated use {@link #beginValidation(InstrumentationValidationParameters, InstrumentationState)} instead */ @Deprecated + @DeprecatedAt("2022-07-26") @NotNull default InstrumentationContext> beginValidation(InstrumentationValidationParameters parameters) { return noOp(); @@ -160,6 +165,7 @@ default InstrumentationContext> beginValidation(Instrument * @deprecated use {@link #beginExecuteOperation(InstrumentationExecuteOperationParameters, InstrumentationState)} instead */ @Deprecated + @DeprecatedAt("2022-07-26") @NotNull default InstrumentationContext beginExecuteOperation(InstrumentationExecuteOperationParameters parameters) { return noOp(); @@ -189,6 +195,7 @@ default InstrumentationContext beginExecuteOperation(Instrument * @deprecated use {@link #beginExecutionStrategy(InstrumentationExecutionStrategyParameters, InstrumentationState)} instead */ @Deprecated + @DeprecatedAt("2022-07-26") @NotNull default ExecutionStrategyInstrumentationContext beginExecutionStrategy(InstrumentationExecutionStrategyParameters parameters) { return ExecutionStrategyInstrumentationContext.NOOP; @@ -219,6 +226,7 @@ default ExecutionStrategyInstrumentationContext beginExecutionStrategy(Instrumen * @deprecated use {@link #beginSubscribedFieldEvent(InstrumentationFieldParameters, InstrumentationState)} instead */ @Deprecated + @DeprecatedAt("2022-07-26") @NotNull default InstrumentationContext beginSubscribedFieldEvent(InstrumentationFieldParameters parameters) { return noOp(); @@ -247,6 +255,7 @@ default InstrumentationContext beginSubscribedFieldEvent(Instru * @deprecated use {@link #beginField(InstrumentationFieldParameters, InstrumentationState)} instead */ @Deprecated + @DeprecatedAt("2022-07-26") @NotNull default InstrumentationContext beginField(InstrumentationFieldParameters parameters) { return noOp(); @@ -275,6 +284,7 @@ default InstrumentationContext beginField(InstrumentationFieldP * @deprecated use {@link #beginFieldFetch(InstrumentationFieldFetchParameters, InstrumentationState)} instead */ @Deprecated + @DeprecatedAt("2022-07-26") @NotNull default InstrumentationContext beginFieldFetch(InstrumentationFieldFetchParameters parameters) { return noOp(); @@ -304,6 +314,7 @@ default InstrumentationContext beginFieldFetch(InstrumentationFieldFetch * @deprecated use {@link #beginFieldComplete(InstrumentationFieldCompleteParameters, InstrumentationState)} instead */ @Deprecated + @DeprecatedAt("2022-07-26") @NotNull default InstrumentationContext beginFieldComplete(InstrumentationFieldCompleteParameters parameters) { return noOp(); @@ -332,6 +343,7 @@ default InstrumentationContext beginFieldComplete(Instrumentati * @deprecated use {@link #beginFieldListComplete(InstrumentationFieldCompleteParameters, InstrumentationState)} instead */ @Deprecated + @DeprecatedAt("2022-07-26") @NotNull default InstrumentationContext beginFieldListComplete(InstrumentationFieldCompleteParameters parameters) { return noOp(); @@ -362,6 +374,7 @@ default InstrumentationContext beginFieldListComplete(Instrumen * @deprecated use {@link #instrumentExecutionInput(ExecutionInput, InstrumentationExecutionParameters, InstrumentationState)} instead */ @Deprecated + @DeprecatedAt("2022-07-26") @NotNull default ExecutionInput instrumentExecutionInput(ExecutionInput executionInput, InstrumentationExecutionParameters parameters) { return executionInput; @@ -393,6 +406,7 @@ default ExecutionInput instrumentExecutionInput(ExecutionInput executionInput, I * @deprecated use {@link #instrumentDocumentAndVariables(DocumentAndVariables, InstrumentationExecutionParameters, InstrumentationState)} instead */ @Deprecated + @DeprecatedAt("2022-07-26") @NotNull default DocumentAndVariables instrumentDocumentAndVariables(DocumentAndVariables documentAndVariables, InstrumentationExecutionParameters parameters) { return documentAndVariables; @@ -424,6 +438,7 @@ default DocumentAndVariables instrumentDocumentAndVariables(DocumentAndVariables * @deprecated use {@link #instrumentSchema(GraphQLSchema, InstrumentationExecutionParameters, InstrumentationState)} instead */ @Deprecated + @DeprecatedAt("2022-07-26") @NotNull default GraphQLSchema instrumentSchema(GraphQLSchema schema, InstrumentationExecutionParameters parameters) { return schema; @@ -453,9 +468,10 @@ default GraphQLSchema instrumentSchema(GraphQLSchema schema, InstrumentationExec * * @return a non null instrumented ExecutionContext, the default is to return to the same object * - * @deprecated use {@link #instrumentExecutionContext(ExecutionContext, InstrumentationExecutionParameters)} instead + * @deprecated use {@link #instrumentExecutionContext(ExecutionContext, InstrumentationExecutionParameters, InstrumentationState)} instead */ @Deprecated + @DeprecatedAt("2022-07-26") @NotNull default ExecutionContext instrumentExecutionContext(ExecutionContext executionContext, InstrumentationExecutionParameters parameters) { return executionContext; @@ -491,6 +507,7 @@ default ExecutionContext instrumentExecutionContext(ExecutionContext executionCo * @deprecated use {@link #instrumentDataFetcher(DataFetcher, InstrumentationFieldFetchParameters, InstrumentationState)} instead */ @Deprecated + @DeprecatedAt("2022-07-26") @NotNull default DataFetcher instrumentDataFetcher(DataFetcher dataFetcher, InstrumentationFieldFetchParameters parameters) { return dataFetcher; @@ -524,6 +541,7 @@ default DataFetcher instrumentDataFetcher(DataFetcher dataFetcher, Instrum * @deprecated use {@link #instrumentExecutionResult(ExecutionResult, InstrumentationExecutionParameters, InstrumentationState)} instead */ @Deprecated + @DeprecatedAt("2022-07-26") @NotNull default CompletableFuture instrumentExecutionResult(ExecutionResult executionResult, InstrumentationExecutionParameters parameters) { return CompletableFuture.completedFuture(executionResult); diff --git a/src/main/java/graphql/execution/instrumentation/InstrumentationState.java b/src/main/java/graphql/execution/instrumentation/InstrumentationState.java index 2bd9c21573..afea0f4afb 100644 --- a/src/main/java/graphql/execution/instrumentation/InstrumentationState.java +++ b/src/main/java/graphql/execution/instrumentation/InstrumentationState.java @@ -10,4 +10,17 @@ */ @PublicSpi public interface InstrumentationState { + + /** + * This helper method allows you to cast from {@link InstrumentationState} to a custom classes more easily. + * + * @param rawState the raw InstrumentationState + * @param for two + * + * @return a cast custom InstrumentationState + */ + static T ofState(InstrumentationState rawState) { + //noinspection unchecked + return (T) rawState; + } } diff --git a/src/main/java/graphql/execution/instrumentation/SimpleInstrumentation.java b/src/main/java/graphql/execution/instrumentation/SimpleInstrumentation.java index ea096c4fcb..a8b27a8718 100644 --- a/src/main/java/graphql/execution/instrumentation/SimpleInstrumentation.java +++ b/src/main/java/graphql/execution/instrumentation/SimpleInstrumentation.java @@ -1,24 +1,19 @@ package graphql.execution.instrumentation; -import graphql.ExecutionResult; +import graphql.DeprecatedAt; import graphql.PublicApi; -import graphql.execution.instrumentation.parameters.InstrumentationExecuteOperationParameters; -import graphql.execution.instrumentation.parameters.InstrumentationExecutionParameters; -import graphql.execution.instrumentation.parameters.InstrumentationExecutionStrategyParameters; -import graphql.execution.instrumentation.parameters.InstrumentationFieldFetchParameters; -import graphql.execution.instrumentation.parameters.InstrumentationFieldParameters; -import graphql.execution.instrumentation.parameters.InstrumentationValidationParameters; -import graphql.language.Document; -import graphql.validation.ValidationError; - -import java.util.List; -import java.util.concurrent.CompletableFuture; /** * An implementation of {@link graphql.execution.instrumentation.Instrumentation} that does nothing. It can be used - * as a base for derived classes where you only implement the methods you want to + * as a base for derived classes where you only implement the methods you want to. With all the methods in {@link Instrumentation} + * now defaulted (post Java 6) this class is really not needed anymore but has been retained for backwards compatibility + * reasons. + * + * @deprecated use {@link SimplePerformantInstrumentation} instead as a base class. */ @PublicApi +@Deprecated +@DeprecatedAt(value = "2022-10-05") public class SimpleInstrumentation implements Instrumentation { /** @@ -26,48 +21,4 @@ public class SimpleInstrumentation implements Instrumentation { */ public static final SimpleInstrumentation INSTANCE = new SimpleInstrumentation(); - @Override - public InstrumentationContext beginExecution(InstrumentationExecutionParameters parameters) { - return SimpleInstrumentationContext.noOp(); - } - - @Override - public InstrumentationContext beginParse(InstrumentationExecutionParameters parameters) { - return SimpleInstrumentationContext.noOp(); - } - - @Override - public InstrumentationContext> beginValidation(InstrumentationValidationParameters parameters) { - return SimpleInstrumentationContext.noOp(); - } - - @Override - public ExecutionStrategyInstrumentationContext beginExecutionStrategy(InstrumentationExecutionStrategyParameters parameters) { - return new ExecutionStrategyInstrumentationContext() { - @Override - public void onDispatched(CompletableFuture result) { - - } - - @Override - public void onCompleted(ExecutionResult result, Throwable t) { - - } - }; - } - - @Override - public InstrumentationContext beginExecuteOperation(InstrumentationExecuteOperationParameters parameters) { - return SimpleInstrumentationContext.noOp(); - } - - @Override - public InstrumentationContext beginField(InstrumentationFieldParameters parameters) { - return SimpleInstrumentationContext.noOp(); - } - - @Override - public InstrumentationContext beginFieldFetch(InstrumentationFieldFetchParameters parameters) { - return SimpleInstrumentationContext.noOp(); - } } diff --git a/src/main/java/graphql/execution/instrumentation/SimplePerformantInstrumentation.java b/src/main/java/graphql/execution/instrumentation/SimplePerformantInstrumentation.java new file mode 100644 index 0000000000..b7835ffddd --- /dev/null +++ b/src/main/java/graphql/execution/instrumentation/SimplePerformantInstrumentation.java @@ -0,0 +1,218 @@ +package graphql.execution.instrumentation; + +import graphql.ExecutionInput; +import graphql.ExecutionResult; +import graphql.PublicApi; +import graphql.execution.ExecutionContext; +import graphql.execution.instrumentation.parameters.InstrumentationCreateStateParameters; +import graphql.execution.instrumentation.parameters.InstrumentationExecuteOperationParameters; +import graphql.execution.instrumentation.parameters.InstrumentationExecutionParameters; +import graphql.execution.instrumentation.parameters.InstrumentationExecutionStrategyParameters; +import graphql.execution.instrumentation.parameters.InstrumentationFieldCompleteParameters; +import graphql.execution.instrumentation.parameters.InstrumentationFieldFetchParameters; +import graphql.execution.instrumentation.parameters.InstrumentationFieldParameters; +import graphql.execution.instrumentation.parameters.InstrumentationValidationParameters; +import graphql.language.Document; +import graphql.schema.DataFetcher; +import graphql.schema.GraphQLSchema; +import graphql.validation.ValidationError; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +import java.util.List; +import java.util.concurrent.CompletableFuture; + +import static graphql.Assert.assertShouldNeverHappen; +import static graphql.execution.instrumentation.SimpleInstrumentationContext.noOp; + +/** + * An implementation of {@link Instrumentation} that does nothing. It can be used + * as a base for derived classes where you only implement the methods you want to. The reason this + * class is designated as more performant is that it does not delegate back to the deprecated methods + * and allocate a new state object per call. + *

+ * This behavior was left in place for backwards compatibility reasons inside {@link Instrumentation} + * and {@link SimpleInstrumentation} but has not been done in this class since no existing classes + * could have derived from it. If you want more performant behavior on methods you don't implement + * then this is the base class to use, since it will not delegate back to old methods + * and cause a new state to be allocated. + */ +@SuppressWarnings("deprecation") +@PublicApi +public class SimplePerformantInstrumentation implements Instrumentation { + + /** + * A singleton instance of a {@link Instrumentation} that does nothing + */ + public static final SimplePerformantInstrumentation INSTANCE = new SimplePerformantInstrumentation(); + + @Override + public InstrumentationState createState() { + return assertShouldNeverHappen("The deprecated " + "createState" + " was called"); + } + + @Override + public @Nullable InstrumentationState createState(InstrumentationCreateStateParameters parameters) { + return null; + } + + @Override + public @NotNull InstrumentationContext beginExecution(InstrumentationExecutionParameters parameters) { + return assertShouldNeverHappen("The deprecated " + "beginExecution" + " was called"); + } + + @Override + public @Nullable InstrumentationContext beginExecution(InstrumentationExecutionParameters parameters, InstrumentationState state) { + return noOp(); + } + + @Override + public @NotNull InstrumentationContext beginParse(InstrumentationExecutionParameters parameters) { + return assertShouldNeverHappen("The deprecated " + "beginParse" + " was called"); + } + + @Override + public @Nullable InstrumentationContext beginParse(InstrumentationExecutionParameters parameters, InstrumentationState state) { + return noOp(); + } + + @Override + public @NotNull InstrumentationContext> beginValidation(InstrumentationValidationParameters parameters) { + return assertShouldNeverHappen("The deprecated " + "beginValidation" + " was called"); + } + + @Override + public @Nullable InstrumentationContext> beginValidation(InstrumentationValidationParameters parameters, InstrumentationState state) { + return noOp(); + } + + @Override + public @NotNull InstrumentationContext beginExecuteOperation(InstrumentationExecuteOperationParameters parameters) { + return assertShouldNeverHappen("The deprecated " + "beginExecuteOperation" + " was called"); + } + + @Override + public @Nullable InstrumentationContext beginExecuteOperation(InstrumentationExecuteOperationParameters parameters, InstrumentationState state) { + return noOp(); + } + + @Override + public @NotNull ExecutionStrategyInstrumentationContext beginExecutionStrategy(InstrumentationExecutionStrategyParameters parameters) { + return assertShouldNeverHappen("The deprecated " + "beginExecutionStrategy" + " was called"); + } + + @Override + public @Nullable ExecutionStrategyInstrumentationContext beginExecutionStrategy(InstrumentationExecutionStrategyParameters parameters, InstrumentationState state) { + return ExecutionStrategyInstrumentationContext.NOOP; + } + + @Override + public @NotNull InstrumentationContext beginSubscribedFieldEvent(InstrumentationFieldParameters parameters) { + return assertShouldNeverHappen("The deprecated " + "beginSubscribedFieldEvent" + " was called"); + } + + @Override + public @Nullable InstrumentationContext beginSubscribedFieldEvent(InstrumentationFieldParameters parameters, InstrumentationState state) { + return noOp(); + } + + @Override + public @NotNull InstrumentationContext beginField(InstrumentationFieldParameters parameters) { + return assertShouldNeverHappen("The deprecated " + "beginField" + " was called"); + } + + @Override + public @Nullable InstrumentationContext beginField(InstrumentationFieldParameters parameters, InstrumentationState state) { + return noOp(); + } + + @Override + public @NotNull InstrumentationContext beginFieldFetch(InstrumentationFieldFetchParameters parameters) { + return assertShouldNeverHappen("The deprecated " + "beginFieldFetch" + " was called"); + } + + @Override + public @Nullable InstrumentationContext beginFieldFetch(InstrumentationFieldFetchParameters parameters, InstrumentationState state) { + return noOp(); + } + + @Override + public @NotNull InstrumentationContext beginFieldComplete(InstrumentationFieldCompleteParameters parameters) { + return assertShouldNeverHappen("The deprecated " + "beginFieldComplete" + " was called"); + } + + @Override + public @Nullable InstrumentationContext beginFieldComplete(InstrumentationFieldCompleteParameters parameters, InstrumentationState state) { + return noOp(); + } + + @Override + public @NotNull InstrumentationContext beginFieldListComplete(InstrumentationFieldCompleteParameters parameters) { + return assertShouldNeverHappen("The deprecated " + "beginFieldListComplete" + " was called"); + } + + @Override + public @Nullable InstrumentationContext beginFieldListComplete(InstrumentationFieldCompleteParameters parameters, InstrumentationState state) { + return noOp(); + } + + @Override + public @NotNull ExecutionInput instrumentExecutionInput(ExecutionInput executionInput, InstrumentationExecutionParameters parameters) { + return assertShouldNeverHappen("The deprecated " + "instrumentExecutionInput" + " was called"); + } + + @Override + public @NotNull ExecutionInput instrumentExecutionInput(ExecutionInput executionInput, InstrumentationExecutionParameters parameters, InstrumentationState state) { + return executionInput; + } + + @Override + public @NotNull DocumentAndVariables instrumentDocumentAndVariables(DocumentAndVariables documentAndVariables, InstrumentationExecutionParameters parameters) { + return assertShouldNeverHappen("The deprecated " + "instrumentDocumentAndVariables" + " was called"); + } + + @Override + public @NotNull DocumentAndVariables instrumentDocumentAndVariables(DocumentAndVariables documentAndVariables, InstrumentationExecutionParameters parameters, InstrumentationState state) { + return documentAndVariables; + } + + @Override + public @NotNull GraphQLSchema instrumentSchema(GraphQLSchema schema, InstrumentationExecutionParameters parameters) { + return assertShouldNeverHappen("The deprecated " + "instrumentSchema" + " was called"); + } + + @Override + public @NotNull GraphQLSchema instrumentSchema(GraphQLSchema schema, InstrumentationExecutionParameters parameters, InstrumentationState state) { + return schema; + } + + @Override + public @NotNull ExecutionContext instrumentExecutionContext(ExecutionContext executionContext, InstrumentationExecutionParameters parameters) { + return assertShouldNeverHappen("The deprecated " + "instrumentExecutionContext" + " was called"); + } + + @Override + public @NotNull ExecutionContext instrumentExecutionContext(ExecutionContext executionContext, InstrumentationExecutionParameters parameters, InstrumentationState state) { + return executionContext; + } + + @Override + public @NotNull DataFetcher instrumentDataFetcher(DataFetcher dataFetcher, InstrumentationFieldFetchParameters parameters) { + return assertShouldNeverHappen("The deprecated " + "instrumentDataFetcher" + " was called"); + } + + @Override + public @NotNull DataFetcher instrumentDataFetcher(DataFetcher dataFetcher, InstrumentationFieldFetchParameters parameters, InstrumentationState state) { + return dataFetcher; + } + + @Override + public @NotNull CompletableFuture instrumentExecutionResult(ExecutionResult executionResult, InstrumentationExecutionParameters parameters) { + return assertShouldNeverHappen("The deprecated " + "instrumentExecutionResult" + " was called"); + } + + @Override + public @NotNull CompletableFuture instrumentExecutionResult(ExecutionResult executionResult, InstrumentationExecutionParameters parameters, InstrumentationState state) { + return CompletableFuture.completedFuture(executionResult); + } +} diff --git a/src/main/java/graphql/execution/instrumentation/dataloader/DataLoaderDispatcherInstrumentation.java b/src/main/java/graphql/execution/instrumentation/dataloader/DataLoaderDispatcherInstrumentation.java index 1822fb4cc4..fd328c3ff5 100644 --- a/src/main/java/graphql/execution/instrumentation/dataloader/DataLoaderDispatcherInstrumentation.java +++ b/src/main/java/graphql/execution/instrumentation/dataloader/DataLoaderDispatcherInstrumentation.java @@ -10,8 +10,7 @@ import graphql.execution.instrumentation.ExecutionStrategyInstrumentationContext; import graphql.execution.instrumentation.InstrumentationContext; import graphql.execution.instrumentation.InstrumentationState; -import graphql.execution.instrumentation.SimpleInstrumentation; -import graphql.execution.instrumentation.SimpleInstrumentationContext; +import graphql.execution.instrumentation.SimplePerformantInstrumentation; import graphql.execution.instrumentation.parameters.InstrumentationCreateStateParameters; import graphql.execution.instrumentation.parameters.InstrumentationExecuteOperationParameters; import graphql.execution.instrumentation.parameters.InstrumentationExecutionParameters; @@ -22,6 +21,8 @@ import org.dataloader.DataLoader; import org.dataloader.DataLoaderRegistry; import org.dataloader.stats.Statistics; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -29,6 +30,9 @@ import java.util.Map; import java.util.concurrent.CompletableFuture; +import static graphql.execution.instrumentation.InstrumentationState.ofState; +import static graphql.execution.instrumentation.SimpleInstrumentationContext.noOp; + /** * This graphql {@link graphql.execution.instrumentation.Instrumentation} will dispatch * all the contained {@link org.dataloader.DataLoader}s when each level of the graphql @@ -44,7 +48,7 @@ * @see org.dataloader.DataLoaderRegistry */ @PublicApi -public class DataLoaderDispatcherInstrumentation extends SimpleInstrumentation { +public class DataLoaderDispatcherInstrumentation extends SimplePerformantInstrumentation { private static final Logger log = LoggerFactory.getLogger(DataLoaderDispatcherInstrumentation.class); @@ -73,14 +77,14 @@ public InstrumentationState createState(InstrumentationCreateStateParameters par } @Override - public DataFetcher instrumentDataFetcher(DataFetcher dataFetcher, InstrumentationFieldFetchParameters parameters) { - DataLoaderDispatcherInstrumentationState state = parameters.getInstrumentationState(); + public @NotNull DataFetcher instrumentDataFetcher(DataFetcher dataFetcher, InstrumentationFieldFetchParameters parameters, InstrumentationState rawState) { + DataLoaderDispatcherInstrumentationState state = ofState(rawState); if (state.isAggressivelyBatching()) { return dataFetcher; } // // currently only AsyncExecutionStrategy with DataLoader and hence this allows us to "dispatch" - // on every object if its not using aggressive batching for other execution strategies + // on every object if it's not using aggressive batching for other execution strategies // which allows them to work if used. return (DataFetcher) environment -> { Object obj = dataFetcher.get(environment); @@ -94,8 +98,8 @@ private void immediatelyDispatch(DataLoaderDispatcherInstrumentationState state) } @Override - public InstrumentationContext beginExecuteOperation(InstrumentationExecuteOperationParameters parameters) { - DataLoaderDispatcherInstrumentationState state = parameters.getInstrumentationState(); + public @Nullable InstrumentationContext beginExecuteOperation(InstrumentationExecuteOperationParameters parameters, InstrumentationState rawState) { + DataLoaderDispatcherInstrumentationState state = ofState(rawState); // // during #instrumentExecutionInput they could have enhanced the data loader registry // so we grab it now just before the query operation gets started @@ -105,7 +109,7 @@ public InstrumentationContext beginExecuteOperation(Instrumenta if (!isDataLoaderCompatibleExecution(parameters.getExecutionContext())) { state.setAggressivelyBatching(false); } - return new SimpleInstrumentationContext<>(); + return noOp(); } private boolean isDataLoaderCompatibleExecution(ExecutionContext executionContext) { @@ -119,8 +123,8 @@ private boolean isDataLoaderCompatibleExecution(ExecutionContext executionContex } @Override - public ExecutionStrategyInstrumentationContext beginExecutionStrategy(InstrumentationExecutionStrategyParameters parameters) { - DataLoaderDispatcherInstrumentationState state = parameters.getInstrumentationState(); + public @Nullable ExecutionStrategyInstrumentationContext beginExecutionStrategy(InstrumentationExecutionStrategyParameters parameters, InstrumentationState rawState) { + DataLoaderDispatcherInstrumentationState state = ofState(rawState); // // if there are no data loaders, there is nothing to do // @@ -136,28 +140,28 @@ public void onCompleted(ExecutionResult result, Throwable t) { }; } - return state.getApproach().beginExecutionStrategy(parameters.withNewState(state.getState())); + return state.getApproach().beginExecutionStrategy(parameters, state.getState()); } @Override - public InstrumentationContext beginFieldFetch(InstrumentationFieldFetchParameters parameters) { - DataLoaderDispatcherInstrumentationState state = parameters.getInstrumentationState(); + public @Nullable InstrumentationContext beginFieldFetch(InstrumentationFieldFetchParameters parameters, InstrumentationState rawState) { + DataLoaderDispatcherInstrumentationState state = ofState(rawState); // // if there are no data loaders, there is nothing to do // if (state.hasNoDataLoaders()) { - return new SimpleInstrumentationContext<>(); + return noOp(); } - return state.getApproach().beginFieldFetch(parameters.withNewState(state.getState())); + return state.getApproach().beginFieldFetch(parameters, state.getState()); } @Override - public CompletableFuture instrumentExecutionResult(ExecutionResult executionResult, InstrumentationExecutionParameters parameters) { + public @NotNull CompletableFuture instrumentExecutionResult(ExecutionResult executionResult, InstrumentationExecutionParameters parameters, InstrumentationState rawState) { if (!options.isIncludeStatistics()) { return CompletableFuture.completedFuture(executionResult); } - DataLoaderDispatcherInstrumentationState state = parameters.getInstrumentationState(); + DataLoaderDispatcherInstrumentationState state = ofState(rawState); Map currentExt = executionResult.getExtensions(); Map statsMap = new LinkedHashMap<>(currentExt == null ? ImmutableKit.emptyMap() : currentExt); Map dataLoaderStats = buildStatsMap(state); diff --git a/src/main/java/graphql/execution/instrumentation/dataloader/FieldLevelTrackingApproach.java b/src/main/java/graphql/execution/instrumentation/dataloader/FieldLevelTrackingApproach.java index 49bb11ed67..e6e9a5330a 100644 --- a/src/main/java/graphql/execution/instrumentation/dataloader/FieldLevelTrackingApproach.java +++ b/src/main/java/graphql/execution/instrumentation/dataloader/FieldLevelTrackingApproach.java @@ -76,13 +76,13 @@ boolean allFetchesHappened(int level) { @Override public String toString() { return "CallStack{" + - "expectedFetchCountPerLevel=" + expectedFetchCountPerLevel + - ", fetchCountPerLevel=" + fetchCountPerLevel + - ", expectedStrategyCallsPerLevel=" + expectedStrategyCallsPerLevel + - ", happenedStrategyCallsPerLevel=" + happenedStrategyCallsPerLevel + - ", happenedOnFieldValueCallsPerLevel=" + happenedOnFieldValueCallsPerLevel + - ", dispatchedLevels" + dispatchedLevels + - '}'; + "expectedFetchCountPerLevel=" + expectedFetchCountPerLevel + + ", fetchCountPerLevel=" + fetchCountPerLevel + + ", expectedStrategyCallsPerLevel=" + expectedStrategyCallsPerLevel + + ", happenedStrategyCallsPerLevel=" + happenedStrategyCallsPerLevel + + ", happenedOnFieldValueCallsPerLevel=" + happenedOnFieldValueCallsPerLevel + + ", dispatchedLevels" + dispatchedLevels + + '}'; } public boolean dispatchIfNotDispatchedBefore(int level) { @@ -118,8 +118,8 @@ public InstrumentationState createState() { return new CallStack(); } - ExecutionStrategyInstrumentationContext beginExecutionStrategy(InstrumentationExecutionStrategyParameters parameters) { - CallStack callStack = parameters.getInstrumentationState(); + ExecutionStrategyInstrumentationContext beginExecutionStrategy(InstrumentationExecutionStrategyParameters parameters, InstrumentationState rawState) { + CallStack callStack = (CallStack) rawState; ResultPath path = parameters.getExecutionStrategyParameters().getPath(); int parentLevel = path.getLevel(); int curLevel = parentLevel + 1; @@ -183,8 +183,8 @@ private int getCountForList(List fieldValueInfos) { } - public InstrumentationContext beginFieldFetch(InstrumentationFieldFetchParameters parameters) { - CallStack callStack = parameters.getInstrumentationState(); + public InstrumentationContext beginFieldFetch(InstrumentationFieldFetchParameters parameters, InstrumentationState rawState) { + CallStack callStack = (CallStack) rawState; ResultPath path = parameters.getEnvironment().getExecutionStepInfo().getPath(); int level = path.getLevel(); return new InstrumentationContext() { @@ -228,7 +228,7 @@ private boolean levelReady(CallStack callStack, int level) { return callStack.allFetchesHappened(1); } if (levelReady(callStack, level - 1) && callStack.allOnFieldCallsHappened(level - 1) - && callStack.allStrategyCallsHappened(level) && callStack.allFetchesHappened(level)) { + && callStack.allStrategyCallsHappened(level) && callStack.allFetchesHappened(level)) { return true; } return false; diff --git a/src/main/java/graphql/execution/instrumentation/fieldvalidation/FieldValidationInstrumentation.java b/src/main/java/graphql/execution/instrumentation/fieldvalidation/FieldValidationInstrumentation.java index 19fa513628..7dc5d6ba72 100644 --- a/src/main/java/graphql/execution/instrumentation/fieldvalidation/FieldValidationInstrumentation.java +++ b/src/main/java/graphql/execution/instrumentation/fieldvalidation/FieldValidationInstrumentation.java @@ -5,8 +5,10 @@ import graphql.PublicApi; import graphql.execution.AbortExecutionException; import graphql.execution.instrumentation.InstrumentationContext; -import graphql.execution.instrumentation.SimpleInstrumentation; +import graphql.execution.instrumentation.InstrumentationState; +import graphql.execution.instrumentation.SimplePerformantInstrumentation; import graphql.execution.instrumentation.parameters.InstrumentationExecuteOperationParameters; +import org.jetbrains.annotations.Nullable; import java.util.List; @@ -22,7 +24,7 @@ * @see FieldValidation */ @PublicApi -public class FieldValidationInstrumentation extends SimpleInstrumentation { +public class FieldValidationInstrumentation extends SimplePerformantInstrumentation { private final FieldValidation fieldValidation; @@ -36,8 +38,7 @@ public FieldValidationInstrumentation(FieldValidation fieldValidation) { } @Override - public InstrumentationContext beginExecuteOperation(InstrumentationExecuteOperationParameters parameters) { - + public @Nullable InstrumentationContext beginExecuteOperation(InstrumentationExecuteOperationParameters parameters, InstrumentationState state) { List errors = FieldValidationSupport.validateFieldsAndArguments(fieldValidation, parameters.getExecutionContext()); if (errors != null && !errors.isEmpty()) { throw new AbortExecutionException(errors); diff --git a/src/main/java/graphql/execution/instrumentation/nextgen/Instrumentation.java b/src/main/java/graphql/execution/instrumentation/nextgen/Instrumentation.java deleted file mode 100644 index 3e59616284..0000000000 --- a/src/main/java/graphql/execution/instrumentation/nextgen/Instrumentation.java +++ /dev/null @@ -1,52 +0,0 @@ -package graphql.execution.instrumentation.nextgen; - -import graphql.ExecutionInput; -import graphql.ExecutionResult; -import graphql.Internal; -import graphql.execution.instrumentation.DocumentAndVariables; -import graphql.execution.instrumentation.InstrumentationContext; -import graphql.execution.instrumentation.InstrumentationState; -import graphql.language.Document; -import graphql.schema.GraphQLSchema; -import graphql.validation.ValidationError; - -import java.util.List; - -import static graphql.execution.instrumentation.SimpleInstrumentationContext.noOp; - -@Internal -public interface Instrumentation { - - default InstrumentationState createState(InstrumentationCreateStateParameters parameters) { - return new InstrumentationState() { - }; - } - - default ExecutionInput instrumentExecutionInput(ExecutionInput executionInput, InstrumentationExecutionParameters parameters) { - return executionInput; - } - - default DocumentAndVariables instrumentDocumentAndVariables(DocumentAndVariables documentAndVariables, InstrumentationExecutionParameters parameters) { - return documentAndVariables; - } - - default GraphQLSchema instrumentSchema(GraphQLSchema graphQLSchema, InstrumentationExecutionParameters parameters) { - return graphQLSchema; - } - - default ExecutionResult instrumentExecutionResult(ExecutionResult result, InstrumentationExecutionParameters parameters) { - return result; - } - - default InstrumentationContext beginExecution(InstrumentationExecutionParameters parameters) { - return noOp(); - } - - default InstrumentationContext beginParse(InstrumentationExecutionParameters parameters) { - return noOp(); - } - - default InstrumentationContext> beginValidation(InstrumentationValidationParameters parameters) { - return noOp(); - } -} diff --git a/src/main/java/graphql/execution/instrumentation/nextgen/InstrumentationCreateStateParameters.java b/src/main/java/graphql/execution/instrumentation/nextgen/InstrumentationCreateStateParameters.java deleted file mode 100644 index ff4ad52cdc..0000000000 --- a/src/main/java/graphql/execution/instrumentation/nextgen/InstrumentationCreateStateParameters.java +++ /dev/null @@ -1,27 +0,0 @@ -package graphql.execution.instrumentation.nextgen; - -import graphql.ExecutionInput; -import graphql.Internal; -import graphql.schema.GraphQLSchema; - -/** - * Parameters sent to {@link graphql.execution.instrumentation.nextgen.Instrumentation} methods - */ -@Internal -public class InstrumentationCreateStateParameters { - private final GraphQLSchema schema; - private final ExecutionInput executionInput; - - public InstrumentationCreateStateParameters(GraphQLSchema schema, ExecutionInput executionInput) { - this.schema = schema; - this.executionInput = executionInput; - } - - public GraphQLSchema getSchema() { - return schema; - } - - public ExecutionInput getExecutionInput() { - return executionInput; - } -} diff --git a/src/main/java/graphql/execution/instrumentation/nextgen/InstrumentationExecutionParameters.java b/src/main/java/graphql/execution/instrumentation/nextgen/InstrumentationExecutionParameters.java deleted file mode 100644 index 4217f09146..0000000000 --- a/src/main/java/graphql/execution/instrumentation/nextgen/InstrumentationExecutionParameters.java +++ /dev/null @@ -1,90 +0,0 @@ -package graphql.execution.instrumentation.nextgen; - -import graphql.ExecutionInput; -import graphql.GraphQLContext; -import graphql.Internal; -import graphql.collect.ImmutableKit; -import graphql.execution.instrumentation.InstrumentationState; -import graphql.schema.GraphQLSchema; - -import java.util.Map; - -/** - * Parameters sent to {@link graphql.execution.instrumentation.nextgen.Instrumentation} methods - */ -@Internal -public class InstrumentationExecutionParameters { - private final ExecutionInput executionInput; - private final String query; - private final String operation; - private final Object context; - private final GraphQLContext graphQLContext; - private final Map variables; - private final InstrumentationState instrumentationState; - private final GraphQLSchema schema; - - public InstrumentationExecutionParameters(ExecutionInput executionInput, GraphQLSchema schema, InstrumentationState instrumentationState) { - this.executionInput = executionInput; - this.query = executionInput.getQuery(); - this.operation = executionInput.getOperationName(); - this.context = executionInput.getContext(); - this.graphQLContext = executionInput.getGraphQLContext(); - this.variables = executionInput.getVariables() != null ? executionInput.getVariables() : ImmutableKit.emptyMap(); - this.instrumentationState = instrumentationState; - this.schema = schema; - } - - /** - * Returns a cloned parameters object with the new state - * - * @param instrumentationState the new state for this parameters object - * - * @return a new parameters object with the new state - */ - public InstrumentationExecutionParameters withNewState(InstrumentationState instrumentationState) { - return new InstrumentationExecutionParameters(this.getExecutionInput(), this.schema, instrumentationState); - } - - public ExecutionInput getExecutionInput() { - return executionInput; - } - - public String getQuery() { - return query; - } - - public String getOperation() { - return operation; - } - - /** - * @param for two - * - * @return the legacy context - * - * @deprecated use {@link #getGraphQLContext()} instead - */ - @Deprecated - @SuppressWarnings({"unchecked", "TypeParameterUnusedInFormals"}) - public T getContext() { - return (T) context; - } - - public GraphQLContext getGraphQLContext() { - return graphQLContext; - } - - public Map getVariables() { - return variables; - } - - @SuppressWarnings("TypeParameterUnusedInFormals") - public T getInstrumentationState() { - //noinspection unchecked - return (T) instrumentationState; - } - - public GraphQLSchema getSchema() { - return this.schema; - } -} diff --git a/src/main/java/graphql/execution/instrumentation/nextgen/InstrumentationValidationParameters.java b/src/main/java/graphql/execution/instrumentation/nextgen/InstrumentationValidationParameters.java deleted file mode 100644 index ebaa26d3b2..0000000000 --- a/src/main/java/graphql/execution/instrumentation/nextgen/InstrumentationValidationParameters.java +++ /dev/null @@ -1,38 +0,0 @@ -package graphql.execution.instrumentation.nextgen; - -import graphql.ExecutionInput; -import graphql.Internal; -import graphql.execution.instrumentation.InstrumentationState; -import graphql.language.Document; -import graphql.schema.GraphQLSchema; - -/** - * Parameters sent to {@link graphql.execution.instrumentation.nextgen.Instrumentation} methods - */ -@Internal -public class InstrumentationValidationParameters extends InstrumentationExecutionParameters { - private final Document document; - - public InstrumentationValidationParameters(ExecutionInput executionInput, Document document, GraphQLSchema schema, InstrumentationState instrumentationState) { - super(executionInput, schema, instrumentationState); - this.document = document; - } - - /** - * Returns a cloned parameters object with the new state - * - * @param instrumentationState the new state for this parameters object - * - * @return a new parameters object with the new state - */ - @Override - public InstrumentationValidationParameters withNewState(InstrumentationState instrumentationState) { - return new InstrumentationValidationParameters( - this.getExecutionInput(), document, getSchema(), instrumentationState); - } - - - public Document getDocument() { - return document; - } -} diff --git a/src/main/java/graphql/execution/instrumentation/nextgen/package-info.java b/src/main/java/graphql/execution/instrumentation/nextgen/package-info.java deleted file mode 100644 index 2f08b060ba..0000000000 --- a/src/main/java/graphql/execution/instrumentation/nextgen/package-info.java +++ /dev/null @@ -1,4 +0,0 @@ -/** - * WARNING: All code in this package is a work in progress for a new execution engine. - */ -package graphql.execution.instrumentation.nextgen; diff --git a/src/main/java/graphql/execution/instrumentation/parameters/InstrumentationExecuteOperationParameters.java b/src/main/java/graphql/execution/instrumentation/parameters/InstrumentationExecuteOperationParameters.java index 5625dbeeee..f8a895657b 100644 --- a/src/main/java/graphql/execution/instrumentation/parameters/InstrumentationExecuteOperationParameters.java +++ b/src/main/java/graphql/execution/instrumentation/parameters/InstrumentationExecuteOperationParameters.java @@ -1,5 +1,6 @@ package graphql.execution.instrumentation.parameters; +import graphql.DeprecatedAt; import graphql.PublicApi; import graphql.execution.ExecutionContext; import graphql.execution.instrumentation.Instrumentation; @@ -33,6 +34,7 @@ private InstrumentationExecuteOperationParameters(ExecutionContext executionCont * @deprecated state is now passed in direct to instrumentation methods */ @Deprecated + @DeprecatedAt("2022-07-26") public InstrumentationExecuteOperationParameters withNewState(InstrumentationState instrumentationState) { return new InstrumentationExecuteOperationParameters(executionContext, instrumentationState); } @@ -52,6 +54,7 @@ public ExecutionContext getExecutionContext() { * @deprecated state is now passed in direct to instrumentation methods */ @Deprecated + @DeprecatedAt("2022-07-26") public T getInstrumentationState() { //noinspection unchecked return (T) instrumentationState; diff --git a/src/main/java/graphql/execution/instrumentation/parameters/InstrumentationExecutionParameters.java b/src/main/java/graphql/execution/instrumentation/parameters/InstrumentationExecutionParameters.java index 6471af7965..57ec489851 100644 --- a/src/main/java/graphql/execution/instrumentation/parameters/InstrumentationExecutionParameters.java +++ b/src/main/java/graphql/execution/instrumentation/parameters/InstrumentationExecutionParameters.java @@ -1,5 +1,6 @@ package graphql.execution.instrumentation.parameters; +import graphql.DeprecatedAt; import graphql.ExecutionInput; import graphql.GraphQLContext; import graphql.PublicApi; @@ -45,6 +46,7 @@ public InstrumentationExecutionParameters(ExecutionInput executionInput, GraphQL * @deprecated state is now passed in direct to instrumentation methods */ @Deprecated + @DeprecatedAt("2022-07-26") public InstrumentationExecutionParameters withNewState(InstrumentationState instrumentationState) { return new InstrumentationExecutionParameters(this.getExecutionInput(), this.schema, instrumentationState); } @@ -69,6 +71,7 @@ public String getOperation() { * @deprecated use {@link #getGraphQLContext()} instead */ @Deprecated + @DeprecatedAt("2021-07-05") @SuppressWarnings({"unchecked", "TypeParameterUnusedInFormals"}) public T getContext() { return (T) context; @@ -93,6 +96,7 @@ public Map getVariables() { * @deprecated state is now passed in direct to instrumentation methods */ @Deprecated + @DeprecatedAt("2022-07-26") @SuppressWarnings("TypeParameterUnusedInFormals") public T getInstrumentationState() { //noinspection unchecked diff --git a/src/main/java/graphql/execution/instrumentation/parameters/InstrumentationExecutionStrategyParameters.java b/src/main/java/graphql/execution/instrumentation/parameters/InstrumentationExecutionStrategyParameters.java index dee0bbf3e5..7f1ec801b3 100644 --- a/src/main/java/graphql/execution/instrumentation/parameters/InstrumentationExecutionStrategyParameters.java +++ b/src/main/java/graphql/execution/instrumentation/parameters/InstrumentationExecutionStrategyParameters.java @@ -1,5 +1,6 @@ package graphql.execution.instrumentation.parameters; +import graphql.DeprecatedAt; import graphql.PublicApi; import graphql.execution.ExecutionContext; import graphql.execution.ExecutionStrategyParameters; @@ -36,6 +37,7 @@ private InstrumentationExecutionStrategyParameters(ExecutionContext executionCon * @deprecated state is now passed in direct to instrumentation methods */ @Deprecated + @DeprecatedAt("2022-07-26") public InstrumentationExecutionStrategyParameters withNewState(InstrumentationState instrumentationState) { return new InstrumentationExecutionStrategyParameters(executionContext, executionStrategyParameters, instrumentationState); } @@ -59,6 +61,7 @@ public ExecutionStrategyParameters getExecutionStrategyParameters() { * @deprecated state is now passed in direct to instrumentation methods */ @Deprecated + @DeprecatedAt("2022-07-26") @SuppressWarnings("TypeParameterUnusedInFormals") public T getInstrumentationState() { //noinspection unchecked diff --git a/src/main/java/graphql/execution/instrumentation/parameters/InstrumentationFieldCompleteParameters.java b/src/main/java/graphql/execution/instrumentation/parameters/InstrumentationFieldCompleteParameters.java index b7221468dd..254e72f47b 100644 --- a/src/main/java/graphql/execution/instrumentation/parameters/InstrumentationFieldCompleteParameters.java +++ b/src/main/java/graphql/execution/instrumentation/parameters/InstrumentationFieldCompleteParameters.java @@ -1,5 +1,6 @@ package graphql.execution.instrumentation.parameters; +import graphql.DeprecatedAt; import graphql.PublicApi; import graphql.execution.ExecutionContext; import graphql.execution.ExecutionStepInfo; @@ -43,6 +44,7 @@ public InstrumentationFieldCompleteParameters(ExecutionContext executionContext, * @deprecated state is now passed in direct to instrumentation methods */ @Deprecated + @DeprecatedAt("2022-07-26") public InstrumentationFieldCompleteParameters withNewState(InstrumentationState instrumentationState) { return new InstrumentationFieldCompleteParameters( this.executionContext, executionStrategyParameters, this.executionStepInfo, this.fetchedValue, instrumentationState); @@ -62,6 +64,7 @@ public GraphQLFieldDefinition getField() { } @Deprecated + @DeprecatedAt("2020-09-08") public ExecutionStepInfo getTypeInfo() { return getExecutionStepInfo(); } @@ -85,6 +88,7 @@ public Object getFetchedValue() { * @deprecated state is now passed in direct to instrumentation methods */ @Deprecated + @DeprecatedAt("2022-07-26") @SuppressWarnings("TypeParameterUnusedInFormals") public T getInstrumentationState() { //noinspection unchecked diff --git a/src/main/java/graphql/execution/instrumentation/parameters/InstrumentationFieldFetchParameters.java b/src/main/java/graphql/execution/instrumentation/parameters/InstrumentationFieldFetchParameters.java index 76fc770c51..6013214013 100644 --- a/src/main/java/graphql/execution/instrumentation/parameters/InstrumentationFieldFetchParameters.java +++ b/src/main/java/graphql/execution/instrumentation/parameters/InstrumentationFieldFetchParameters.java @@ -1,5 +1,6 @@ package graphql.execution.instrumentation.parameters; +import graphql.DeprecatedAt; import graphql.PublicApi; import graphql.execution.ExecutionContext; import graphql.execution.ExecutionStrategyParameters; @@ -7,24 +8,26 @@ import graphql.execution.instrumentation.InstrumentationState; import graphql.schema.DataFetchingEnvironment; +import java.util.function.Supplier; + /** * Parameters sent to {@link Instrumentation} methods */ @PublicApi public class InstrumentationFieldFetchParameters extends InstrumentationFieldParameters { - private final DataFetchingEnvironment environment; + private final Supplier environment; private final ExecutionStrategyParameters executionStrategyParameters; private final boolean trivialDataFetcher; - public InstrumentationFieldFetchParameters(ExecutionContext getExecutionContext, DataFetchingEnvironment environment, ExecutionStrategyParameters executionStrategyParameters, boolean trivialDataFetcher) { - super(getExecutionContext, environment::getExecutionStepInfo); + public InstrumentationFieldFetchParameters(ExecutionContext getExecutionContext, Supplier environment, ExecutionStrategyParameters executionStrategyParameters, boolean trivialDataFetcher) { + super(getExecutionContext, () -> environment.get().getExecutionStepInfo()); this.environment = environment; this.executionStrategyParameters = executionStrategyParameters; this.trivialDataFetcher = trivialDataFetcher; } - private InstrumentationFieldFetchParameters(ExecutionContext getExecutionContext, DataFetchingEnvironment environment, InstrumentationState instrumentationState, ExecutionStrategyParameters executionStrategyParameters, boolean trivialDataFetcher) { - super(getExecutionContext, environment::getExecutionStepInfo, instrumentationState); + private InstrumentationFieldFetchParameters(ExecutionContext getExecutionContext, Supplier environment, InstrumentationState instrumentationState, ExecutionStrategyParameters executionStrategyParameters, boolean trivialDataFetcher) { + super(getExecutionContext, () -> environment.get().getExecutionStepInfo(), instrumentationState); this.environment = environment; this.executionStrategyParameters = executionStrategyParameters; this.trivialDataFetcher = trivialDataFetcher; @@ -40,16 +43,17 @@ private InstrumentationFieldFetchParameters(ExecutionContext getExecutionContext * @deprecated state is now passed in direct to instrumentation methods */ @Deprecated + @DeprecatedAt("2022-07-26") @Override public InstrumentationFieldFetchParameters withNewState(InstrumentationState instrumentationState) { return new InstrumentationFieldFetchParameters( - this.getExecutionContext(), this.getEnvironment(), + this.getExecutionContext(), this.environment, instrumentationState, executionStrategyParameters, trivialDataFetcher); } public DataFetchingEnvironment getEnvironment() { - return environment; + return environment.get(); } public boolean isTrivialDataFetcher() { diff --git a/src/main/java/graphql/execution/instrumentation/parameters/InstrumentationFieldParameters.java b/src/main/java/graphql/execution/instrumentation/parameters/InstrumentationFieldParameters.java index a6d6f0bfea..070def45a1 100644 --- a/src/main/java/graphql/execution/instrumentation/parameters/InstrumentationFieldParameters.java +++ b/src/main/java/graphql/execution/instrumentation/parameters/InstrumentationFieldParameters.java @@ -1,5 +1,6 @@ package graphql.execution.instrumentation.parameters; +import graphql.DeprecatedAt; import graphql.PublicApi; import graphql.execution.ExecutionContext; import graphql.execution.ExecutionStepInfo; @@ -38,6 +39,7 @@ public InstrumentationFieldParameters(ExecutionContext executionContext, Supplie * @deprecated state is now passed in direct to instrumentation methods */ @Deprecated + @DeprecatedAt("2022-07-26") public InstrumentationFieldParameters withNewState(InstrumentationState instrumentationState) { return new InstrumentationFieldParameters( this.executionContext, this.executionStepInfo, instrumentationState); @@ -67,6 +69,7 @@ public ExecutionStepInfo getExecutionStepInfo() { * @deprecated state is now passed in direct to instrumentation methods */ @Deprecated + @DeprecatedAt("2022-07-26") @SuppressWarnings("TypeParameterUnusedInFormals") public T getInstrumentationState() { //noinspection unchecked diff --git a/src/main/java/graphql/execution/instrumentation/parameters/InstrumentationValidationParameters.java b/src/main/java/graphql/execution/instrumentation/parameters/InstrumentationValidationParameters.java index 230749b70c..a99619affa 100644 --- a/src/main/java/graphql/execution/instrumentation/parameters/InstrumentationValidationParameters.java +++ b/src/main/java/graphql/execution/instrumentation/parameters/InstrumentationValidationParameters.java @@ -1,5 +1,6 @@ package graphql.execution.instrumentation.parameters; +import graphql.DeprecatedAt; import graphql.ExecutionInput; import graphql.PublicApi; import graphql.execution.instrumentation.Instrumentation; @@ -29,6 +30,7 @@ public InstrumentationValidationParameters(ExecutionInput executionInput, Docume * @deprecated state is now passed in direct to instrumentation methods */ @Deprecated + @DeprecatedAt("2022-07-26") @Override public InstrumentationValidationParameters withNewState(InstrumentationState instrumentationState) { return new InstrumentationValidationParameters( diff --git a/src/main/java/graphql/execution/instrumentation/threadpools/ExecutorInstrumentation.java b/src/main/java/graphql/execution/instrumentation/threadpools/ExecutorInstrumentation.java index d14eec10ff..af210df041 100644 --- a/src/main/java/graphql/execution/instrumentation/threadpools/ExecutorInstrumentation.java +++ b/src/main/java/graphql/execution/instrumentation/threadpools/ExecutorInstrumentation.java @@ -5,10 +5,12 @@ import graphql.Internal; import graphql.TrivialDataFetcher; import graphql.execution.Async; -import graphql.execution.instrumentation.SimpleInstrumentation; +import graphql.execution.instrumentation.InstrumentationState; +import graphql.execution.instrumentation.SimplePerformantInstrumentation; import graphql.execution.instrumentation.parameters.InstrumentationFieldFetchParameters; import graphql.schema.DataFetcher; import graphql.schema.DataFetchingEnvironment; +import org.jetbrains.annotations.NotNull; import java.util.concurrent.CompletableFuture; import java.util.concurrent.CompletionStage; @@ -24,13 +26,13 @@ * This instrumentation can be used to control on what thread calls to {@link DataFetcher}s happen on. *

* If your data fetching is inherently IO bound then you could use a IO oriented thread pool for your fetches and transfer control - * back to a CPU oriented thread pool and allow graphql-java code to run the post processing of results there. + * back to a CPU oriented thread pool and allow graphql-java code to run the post-processing of results there. *

* An IO oriented thread pool is typically a multiple of {@link Runtime#availableProcessors()} while a CPU oriented thread pool * is typically no more than {@link Runtime#availableProcessors()}. *

- * The instrumentation will use the {@link graphql.execution.instrumentation.Instrumentation#instrumentDataFetcher(DataFetcher, InstrumentationFieldFetchParameters)} - * method to change your data fetchers so they are executed on a thread pool dedicated to fetching (if you provide one). + * The instrumentation will use the {@link graphql.execution.instrumentation.Instrumentation#instrumentDataFetcher(DataFetcher, InstrumentationFieldFetchParameters, InstrumentationState)} + * method to change your data fetchers, so they are executed on a thread pool dedicated to fetching (if you provide one). *

* Once the data fetcher value is returns it will transfer control back to a processing thread pool (if you provide one). *

@@ -39,7 +41,7 @@ */ @Internal @Beta -public class ExecutorInstrumentation extends SimpleInstrumentation { +public class ExecutorInstrumentation extends SimplePerformantInstrumentation { private static final Consumer NOOP = a -> { }; @@ -106,7 +108,7 @@ public ExecutorInstrumentation build() { } @Override - public DataFetcher instrumentDataFetcher(DataFetcher originalDataFetcher, InstrumentationFieldFetchParameters parameters) { + public @NotNull DataFetcher instrumentDataFetcher(DataFetcher originalDataFetcher, InstrumentationFieldFetchParameters parameters, InstrumentationState state) { if (originalDataFetcher instanceof TrivialDataFetcher) { return originalDataFetcher; } @@ -117,7 +119,7 @@ public DataFetcher instrumentDataFetcher(DataFetcher originalDataFetcher, // the CF will be left running on that fetch executors thread invokedCF = CompletableFuture.supplyAsync(invokedAsync(originalDataFetcher, environment), fetchExecutor); } else { - invokedCF = invokedSynch(originalDataFetcher, environment); + invokedCF = invokedSync(originalDataFetcher, environment); } if (processingExecutor != null) { invokedCF = invokedCF.thenApplyAsync(processingControl(), processingExecutor); @@ -136,7 +138,7 @@ private Supplier> invokedAsync(DataFetcher originalDataFet }; } - private CompletableFuture> invokedSynch(DataFetcher originalDataFetcher, DataFetchingEnvironment environment) { + private CompletableFuture> invokedSync(DataFetcher originalDataFetcher, DataFetchingEnvironment environment) { actionObserver.accept(FETCHING); return CompletableFuture.completedFuture(invokeOriginalDF(originalDataFetcher, environment)); } diff --git a/src/main/java/graphql/execution/instrumentation/tracing/TracingInstrumentation.java b/src/main/java/graphql/execution/instrumentation/tracing/TracingInstrumentation.java index 80231608b0..9750ec1475 100644 --- a/src/main/java/graphql/execution/instrumentation/tracing/TracingInstrumentation.java +++ b/src/main/java/graphql/execution/instrumentation/tracing/TracingInstrumentation.java @@ -7,19 +7,22 @@ import graphql.execution.instrumentation.Instrumentation; import graphql.execution.instrumentation.InstrumentationContext; import graphql.execution.instrumentation.InstrumentationState; -import graphql.execution.instrumentation.SimpleInstrumentation; +import graphql.execution.instrumentation.SimplePerformantInstrumentation; +import graphql.execution.instrumentation.parameters.InstrumentationCreateStateParameters; import graphql.execution.instrumentation.parameters.InstrumentationExecutionParameters; import graphql.execution.instrumentation.parameters.InstrumentationFieldFetchParameters; import graphql.execution.instrumentation.parameters.InstrumentationValidationParameters; import graphql.language.Document; import graphql.validation.ValidationError; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; -import java.util.Collections; import java.util.LinkedHashMap; import java.util.List; import java.util.Map; import java.util.concurrent.CompletableFuture; +import static graphql.execution.instrumentation.InstrumentationState.ofState; import static graphql.execution.instrumentation.SimpleInstrumentationContext.whenCompleted; /** @@ -27,7 +30,7 @@ * capture tracing information and puts it into the {@link ExecutionResult} */ @PublicApi -public class TracingInstrumentation extends SimpleInstrumentation { +public class TracingInstrumentation extends SimplePerformantInstrumentation { public static class Options { private final boolean includeTrivialDataFetchers; @@ -69,15 +72,15 @@ public TracingInstrumentation(Options options) { private final Options options; @Override - public InstrumentationState createState() { + public @Nullable InstrumentationState createState(InstrumentationCreateStateParameters parameters) { return new TracingSupport(options.includeTrivialDataFetchers); } @Override - public CompletableFuture instrumentExecutionResult(ExecutionResult executionResult, InstrumentationExecutionParameters parameters) { + public @NotNull CompletableFuture instrumentExecutionResult(ExecutionResult executionResult, InstrumentationExecutionParameters parameters, InstrumentationState rawState) { Map currentExt = executionResult.getExtensions(); - TracingSupport tracingSupport = parameters.getInstrumentationState(); + TracingSupport tracingSupport = ofState(rawState); Map withTracingExt = new LinkedHashMap<>(currentExt == null ? ImmutableKit.emptyMap() : currentExt); withTracingExt.put("tracing", tracingSupport.snapshotTracingData()); @@ -85,22 +88,22 @@ public CompletableFuture instrumentExecutionResult(ExecutionRes } @Override - public InstrumentationContext beginFieldFetch(InstrumentationFieldFetchParameters parameters) { - TracingSupport tracingSupport = parameters.getInstrumentationState(); + public InstrumentationContext beginFieldFetch(InstrumentationFieldFetchParameters parameters, InstrumentationState rawState) { + TracingSupport tracingSupport = ofState(rawState); TracingSupport.TracingContext ctx = tracingSupport.beginField(parameters.getEnvironment(), parameters.isTrivialDataFetcher()); return whenCompleted((result, t) -> ctx.onEnd()); } @Override - public InstrumentationContext beginParse(InstrumentationExecutionParameters parameters) { - TracingSupport tracingSupport = parameters.getInstrumentationState(); + public InstrumentationContext beginParse(InstrumentationExecutionParameters parameters, InstrumentationState rawState) { + TracingSupport tracingSupport = ofState(rawState); TracingSupport.TracingContext ctx = tracingSupport.beginParse(); return whenCompleted((result, t) -> ctx.onEnd()); } @Override - public InstrumentationContext> beginValidation(InstrumentationValidationParameters parameters) { - TracingSupport tracingSupport = parameters.getInstrumentationState(); + public InstrumentationContext> beginValidation(InstrumentationValidationParameters parameters, InstrumentationState rawState) { + TracingSupport tracingSupport = ofState(rawState); TracingSupport.TracingContext ctx = tracingSupport.beginValidation(); return whenCompleted((result, t) -> ctx.onEnd()); } diff --git a/src/main/java/graphql/execution/nextgen/BatchedDataFetcher.java b/src/main/java/graphql/execution/nextgen/BatchedDataFetcher.java deleted file mode 100644 index b95954fd63..0000000000 --- a/src/main/java/graphql/execution/nextgen/BatchedDataFetcher.java +++ /dev/null @@ -1,12 +0,0 @@ -package graphql.execution.nextgen; - -import graphql.Internal; -import graphql.schema.DataFetcher; - -/** - * @deprecated Jan 2022 - We have decided to deprecate the NextGen engine, and it will be removed in a future release. - */ -@Deprecated -@Internal -public interface BatchedDataFetcher extends DataFetcher { -} diff --git a/src/main/java/graphql/execution/nextgen/BatchedExecutionStrategy.java b/src/main/java/graphql/execution/nextgen/BatchedExecutionStrategy.java deleted file mode 100644 index a8e1f808fe..0000000000 --- a/src/main/java/graphql/execution/nextgen/BatchedExecutionStrategy.java +++ /dev/null @@ -1,161 +0,0 @@ -package graphql.execution.nextgen; - -import com.google.common.collect.ImmutableList; -import graphql.ExecutionResult; -import graphql.Internal; -import graphql.execution.Async; -import graphql.execution.ExecutionContext; -import graphql.execution.ExecutionStepInfo; -import graphql.execution.ExecutionStepInfoFactory; -import graphql.execution.FetchedValue; -import graphql.execution.MergedField; -import graphql.execution.MergedSelectionSet; -import graphql.execution.nextgen.result.ExecutionResultNode; -import graphql.execution.nextgen.result.ObjectExecutionResultNode; -import graphql.execution.nextgen.result.ResultNodesUtil; -import graphql.execution.nextgen.result.RootExecutionResultNode; -import graphql.execution.nextgen.result.UnresolvedObjectResultNode; -import graphql.util.FpKit; -import graphql.util.NodeMultiZipper; -import graphql.util.NodeZipper; - -import java.util.ArrayList; -import java.util.List; -import java.util.Map; -import java.util.concurrent.CompletableFuture; - -import static graphql.Assert.assertNotEmpty; -import static graphql.Assert.assertTrue; -import static graphql.collect.ImmutableKit.map; -import static graphql.execution.nextgen.result.ResultNodeAdapter.RESULT_NODE_ADAPTER; -import static graphql.util.FpKit.flatList; -import static graphql.util.FpKit.mapEntries; -import static graphql.util.FpKit.transposeMatrix; -import static java.util.concurrent.CompletableFuture.completedFuture; - -/** - * @deprecated Jan 2022 - We have decided to deprecate the NextGen engine, and it will be removed in a future release. - */ -@Deprecated -@Internal -public class BatchedExecutionStrategy implements ExecutionStrategy { - - ExecutionStepInfoFactory executionInfoFactory = new ExecutionStepInfoFactory(); - ValueFetcher valueFetcher = new ValueFetcher(); - - FetchedValueAnalyzer fetchedValueAnalyzer = new FetchedValueAnalyzer(); - ExecutionStrategyUtil util = new ExecutionStrategyUtil(); - ExecutionHelper executionHelper = new ExecutionHelper(); - - - @Override - public CompletableFuture execute(ExecutionContext context) { - FieldSubSelection fieldSubSelection = executionHelper.getFieldSubSelection(context); - return executeImpl(context, fieldSubSelection) - .thenApply(ResultNodesUtil::toExecutionResult); - } - - - public CompletableFuture executeImpl(ExecutionContext executionContext, FieldSubSelection fieldSubSelection) { - CompletableFuture rootCF = Async.each(util.fetchSubSelection(executionContext, fieldSubSelection)) - .thenApply(RootExecutionResultNode::new); - - return rootCF.thenCompose(rootNode -> { - NodeMultiZipper unresolvedNodes = ResultNodesUtil.getUnresolvedNodes(rootNode); - return nextStep(executionContext, unresolvedNodes); - }) - .thenApply(multiZipper -> multiZipper.toRootNode()) - .thenApply(RootExecutionResultNode.class::cast); - } - - - private CompletableFuture> nextStep(ExecutionContext executionContext, NodeMultiZipper multizipper) { - NodeMultiZipper nextUnresolvedNodes = ResultNodesUtil.getUnresolvedNodes(multizipper.toRootNode()); - if (nextUnresolvedNodes.getZippers().size() == 0) { - return completedFuture(nextUnresolvedNodes); - } - List> groups = groupNodesIntoBatches(nextUnresolvedNodes); - return resolveNodes(executionContext, groups).thenCompose(next -> nextStep(executionContext, next)); - } - - // all multizipper have the same root - private CompletableFuture> resolveNodes(ExecutionContext executionContext, List> unresolvedNodes) { - assertNotEmpty(unresolvedNodes, () -> "unresolvedNodes can't be empty"); - ExecutionResultNode commonRoot = unresolvedNodes.get(0).getCommonRoot(); - CompletableFuture>>> listListCF = Async.flatMap(unresolvedNodes, - executionResultMultiZipper -> fetchAndAnalyze(executionContext, executionResultMultiZipper.getZippers())); - - return flatList(listListCF).thenApply(zippers -> new NodeMultiZipper(commonRoot, zippers, RESULT_NODE_ADAPTER)); - } - - private List> groupNodesIntoBatches(NodeMultiZipper unresolvedZipper) { - Map>> zipperBySubSelection = FpKit.groupingBy(unresolvedZipper.getZippers(), - (executionResultZipper -> executionResultZipper.getCurNode().getMergedField())); - return mapEntries(zipperBySubSelection, (key, value) -> new NodeMultiZipper(unresolvedZipper.getCommonRoot(), value, RESULT_NODE_ADAPTER)); - } - - private CompletableFuture>> fetchAndAnalyze(ExecutionContext executionContext, List> unresolvedNodes) { - assertTrue(unresolvedNodes.size() > 0, () -> "unresolvedNodes can't be empty"); - - List fieldSubSelections = map(unresolvedNodes, - node -> util.createFieldSubSelection(executionContext, node.getCurNode().getExecutionStepInfo(), node.getCurNode().getResolvedValue())); - - //constrain: all fieldSubSelections have the same mergedSelectionSet - MergedSelectionSet mergedSelectionSet = fieldSubSelections.get(0).getMergedSelectionSet(); - - List>> fetchedValues = batchFetchForEachSubField(executionContext, fieldSubSelections, mergedSelectionSet); - - return mapBatchedResultsBack(unresolvedNodes, fetchedValues); - } - - private CompletableFuture>> mapBatchedResultsBack(List> unresolvedNodes, List>> fetchedValues) { - return Async.each(fetchedValues).thenApply(fetchedValuesMatrix -> { - List> result = new ArrayList<>(); - List> newChildsPerNode = transposeMatrix(fetchedValuesMatrix); - - for (int i = 0; i < newChildsPerNode.size(); i++) { - NodeZipper unresolvedNodeZipper = unresolvedNodes.get(i); - List fetchedValuesForNode = newChildsPerNode.get(i); - NodeZipper resolvedZipper = resolveZipper(unresolvedNodeZipper, fetchedValuesForNode); - result.add(resolvedZipper); - } - return result; - }); - } - - private List>> batchFetchForEachSubField(ExecutionContext executionContext, - List fieldSubSelections, - MergedSelectionSet mergedSelectionSet) { - List sources = map(fieldSubSelections, FieldSubSelection::getSource); - return mapEntries(mergedSelectionSet.getSubFields(), (name, mergedField) -> { - List newExecutionStepInfos = newExecutionInfos(executionContext, fieldSubSelections, mergedField); - return valueFetcher - .fetchBatchedValues(executionContext, sources, mergedField, newExecutionStepInfos) - .thenApply(fetchValue -> analyseValues(executionContext, fetchValue, newExecutionStepInfos)); - }); - } - - private List newExecutionInfos(ExecutionContext executionContext, List fieldSubSelections, MergedField mergedField) { - return map(fieldSubSelections, - subSelection -> executionInfoFactory.newExecutionStepInfoForSubField(executionContext, mergedField, subSelection.getExecutionStepInfo())); - } - - private NodeZipper resolveZipper(NodeZipper unresolvedNodeZipper, List fetchedValuesForNode) { - UnresolvedObjectResultNode unresolvedNode = (UnresolvedObjectResultNode) unresolvedNodeZipper.getCurNode(); - List newChildren = util.fetchedValueAnalysisToNodes(fetchedValuesForNode); - ObjectExecutionResultNode newNode = unresolvedNode.withNewChildren(newChildren); - return unresolvedNodeZipper.withNewNode(newNode); - } - - - private List analyseValues(ExecutionContext executionContext, List fetchedValues, List executionInfos) { - List result = new ArrayList<>(); - for (int i = 0; i < fetchedValues.size(); i++) { - FetchedValue fetchedValue = fetchedValues.get(i); - ExecutionStepInfo executionStepInfo = executionInfos.get(i); - FetchedValueAnalysis fetchedValueAnalysis = fetchedValueAnalyzer.analyzeFetchedValue(executionContext, fetchedValue, executionStepInfo); - result.add(fetchedValueAnalysis); - } - return result; - } -} diff --git a/src/main/java/graphql/execution/nextgen/Common.java b/src/main/java/graphql/execution/nextgen/Common.java deleted file mode 100644 index 049774ae7b..0000000000 --- a/src/main/java/graphql/execution/nextgen/Common.java +++ /dev/null @@ -1,41 +0,0 @@ -package graphql.execution.nextgen; - -import graphql.Internal; -import graphql.execution.MissingRootTypeException; -import graphql.language.OperationDefinition; -import graphql.schema.GraphQLObjectType; -import graphql.schema.GraphQLSchema; - -import java.util.Optional; - -import static graphql.Assert.assertShouldNeverHappen; -import static graphql.language.OperationDefinition.Operation.MUTATION; -import static graphql.language.OperationDefinition.Operation.QUERY; -import static graphql.language.OperationDefinition.Operation.SUBSCRIPTION; - -/** - * @deprecated Jan 2022 - We have decided to deprecate the NextGen engine, and it will be removed in a future release. - */ -@Deprecated -@Internal -public class Common { - - public static GraphQLObjectType getOperationRootType(GraphQLSchema graphQLSchema, OperationDefinition operationDefinition) { - OperationDefinition.Operation operation = operationDefinition.getOperation(); - if (operation == MUTATION) { - GraphQLObjectType mutationType = graphQLSchema.getMutationType(); - return Optional.ofNullable(mutationType) - .orElseThrow(() -> new MissingRootTypeException("Schema is not configured for mutations.", operationDefinition.getSourceLocation())); - } else if (operation == QUERY) { - GraphQLObjectType queryType = graphQLSchema.getQueryType(); - return Optional.ofNullable(queryType) - .orElseThrow(() -> new MissingRootTypeException("Schema does not define the required query root type.", operationDefinition.getSourceLocation())); - } else if (operation == SUBSCRIPTION) { - GraphQLObjectType subscriptionType = graphQLSchema.getSubscriptionType(); - return Optional.ofNullable(subscriptionType) - .orElseThrow(() -> new MissingRootTypeException("Schema is not configured for subscriptions.", operationDefinition.getSourceLocation())); - } else { - return assertShouldNeverHappen("Unhandled case. An extra operation enum has been added without code support"); - } - } -} diff --git a/src/main/java/graphql/execution/nextgen/DefaultExecutionStrategy.java b/src/main/java/graphql/execution/nextgen/DefaultExecutionStrategy.java deleted file mode 100644 index b01f3709bd..0000000000 --- a/src/main/java/graphql/execution/nextgen/DefaultExecutionStrategy.java +++ /dev/null @@ -1,80 +0,0 @@ -package graphql.execution.nextgen; - -import graphql.ExecutionResult; -import graphql.Internal; -import graphql.execution.ExecutionContext; -import graphql.execution.ExecutionStepInfo; -import graphql.execution.nextgen.result.ExecutionResultNode; -import graphql.execution.nextgen.result.ObjectExecutionResultNode; -import graphql.execution.nextgen.result.ResolvedValue; -import graphql.execution.nextgen.result.ResultNodesUtil; -import graphql.execution.nextgen.result.RootExecutionResultNode; -import graphql.util.NodeMultiZipper; -import graphql.util.NodeZipper; - -import java.util.List; -import java.util.concurrent.CompletableFuture; - -import static graphql.collect.ImmutableKit.map; -import static graphql.execution.Async.each; -import static graphql.execution.Async.mapCompose; - -/** - * - * @deprecated Jan 2022 - We have decided to deprecate the NextGen engine, and it will be removed in a future release. - */ -@Deprecated -@Internal -public class DefaultExecutionStrategy implements ExecutionStrategy { - - ExecutionStrategyUtil util = new ExecutionStrategyUtil(); - ExecutionHelper executionHelper = new ExecutionHelper(); - - @Override - public CompletableFuture execute(ExecutionContext context) { - FieldSubSelection fieldSubSelection = executionHelper.getFieldSubSelection(context); - return executeImpl(context, fieldSubSelection) - .thenApply(ResultNodesUtil::toExecutionResult); - } - - /* - * the fundamental algorithm is: - * - fetch sub selection and analyze it - * - convert the fetched value analysis into result node - * - get all unresolved result nodes and resolve the sub selection (start again recursively) - */ - public CompletableFuture executeImpl(ExecutionContext context, FieldSubSelection fieldSubSelection) { - return resolveSubSelection(context, fieldSubSelection) - .thenApply(RootExecutionResultNode::new); - } - - private CompletableFuture> resolveSubSelection(ExecutionContext executionContext, FieldSubSelection fieldSubSelection) { - List> namedNodesCFList = - mapCompose(util.fetchSubSelection(executionContext, fieldSubSelection), node -> resolveAllChildNodes(executionContext, node)); - return each(namedNodesCFList); - } - - private CompletableFuture resolveAllChildNodes(ExecutionContext context, ExecutionResultNode node) { - NodeMultiZipper unresolvedNodes = ResultNodesUtil.getUnresolvedNodes(node); - List>> resolvedNodes = map(unresolvedNodes.getZippers(), unresolvedNode -> resolveNode(context, unresolvedNode)); - return resolvedNodesToResultNode(unresolvedNodes, resolvedNodes); - } - - private CompletableFuture> resolveNode(ExecutionContext executionContext, NodeZipper unresolvedNode) { - ExecutionStepInfo executionStepInfo = unresolvedNode.getCurNode().getExecutionStepInfo(); - ResolvedValue resolvedValue = unresolvedNode.getCurNode().getResolvedValue(); - FieldSubSelection fieldSubSelection = util.createFieldSubSelection(executionContext, executionStepInfo, resolvedValue); - return resolveSubSelection(executionContext, fieldSubSelection) - .thenApply(resolvedChildMap -> unresolvedNode.withNewNode(new ObjectExecutionResultNode(executionStepInfo, resolvedValue, resolvedChildMap))); - } - - private CompletableFuture resolvedNodesToResultNode( - NodeMultiZipper unresolvedNodes, - List>> resolvedNodes) { - return each(resolvedNodes) - .thenApply(unresolvedNodes::withReplacedZippers) - .thenApply(NodeMultiZipper::toRootNode); - } - - -} diff --git a/src/main/java/graphql/execution/nextgen/Execution.java b/src/main/java/graphql/execution/nextgen/Execution.java deleted file mode 100644 index 4e39298e59..0000000000 --- a/src/main/java/graphql/execution/nextgen/Execution.java +++ /dev/null @@ -1,48 +0,0 @@ -package graphql.execution.nextgen; - -import graphql.ExecutionInput; -import graphql.ExecutionResult; -import graphql.ExecutionResultImpl; -import graphql.GraphQLError; -import graphql.Internal; -import graphql.execution.Async; -import graphql.execution.ExecutionId; -import graphql.execution.instrumentation.InstrumentationState; -import graphql.language.Document; -import graphql.schema.GraphQLSchema; - -import java.util.concurrent.CompletableFuture; - -/** - * @deprecated Jan 2022 - We have decided to deprecate the NextGen engine, and it will be removed in a future release. - */ -@Deprecated -@Internal -public class Execution { - - ExecutionHelper executionHelper = new ExecutionHelper(); - - public CompletableFuture execute(ExecutionStrategy executionStrategy, - Document document, - GraphQLSchema graphQLSchema, - ExecutionId executionId, - ExecutionInput executionInput, - InstrumentationState instrumentationState) { - ExecutionHelper.ExecutionData executionData; - try { - executionData = executionHelper.createExecutionData(document, graphQLSchema, executionId, executionInput, instrumentationState); - } catch (RuntimeException rte) { - if (rte instanceof GraphQLError) { - return CompletableFuture.completedFuture(new ExecutionResultImpl((GraphQLError) rte)); - } - return Async.exceptionallyCompletedFuture(rte); - } - - try { - return executionStrategy - .execute(executionData.executionContext); - } catch (Exception e) { - throw new RuntimeException(e); - } - } -} diff --git a/src/main/java/graphql/execution/nextgen/ExecutionHelper.java b/src/main/java/graphql/execution/nextgen/ExecutionHelper.java deleted file mode 100644 index 19f1ed1e22..0000000000 --- a/src/main/java/graphql/execution/nextgen/ExecutionHelper.java +++ /dev/null @@ -1,98 +0,0 @@ -package graphql.execution.nextgen; - -import graphql.ExecutionInput; -import graphql.Internal; -import graphql.execution.CoercedVariables; -import graphql.execution.ExecutionContext; -import graphql.execution.ExecutionId; -import graphql.execution.ExecutionStepInfo; -import graphql.execution.FieldCollector; -import graphql.execution.FieldCollectorParameters; -import graphql.execution.MergedSelectionSet; -import graphql.execution.RawVariables; -import graphql.execution.ResultPath; -import graphql.execution.ValuesResolver; -import graphql.execution.instrumentation.InstrumentationState; -import graphql.language.Document; -import graphql.language.FragmentDefinition; -import graphql.language.NodeUtil; -import graphql.language.OperationDefinition; -import graphql.language.VariableDefinition; -import graphql.schema.GraphQLObjectType; -import graphql.schema.GraphQLSchema; - -import java.util.List; -import java.util.Map; - -import static graphql.execution.ExecutionContextBuilder.newExecutionContextBuilder; -import static graphql.execution.ExecutionStepInfo.newExecutionStepInfo; - -/** - * @deprecated Jan 2022 - We have decided to deprecate the NextGen engine, and it will be removed in a future release. - */ -@Deprecated -@Internal -public class ExecutionHelper { - - private final FieldCollector fieldCollector = new FieldCollector(); - - public static class ExecutionData { - public ExecutionContext executionContext; - } - - public ExecutionData createExecutionData(Document document, - GraphQLSchema graphQLSchema, - ExecutionId executionId, - ExecutionInput executionInput, - InstrumentationState instrumentationState) { - - NodeUtil.GetOperationResult getOperationResult = NodeUtil.getOperation(document, executionInput.getOperationName()); - Map fragmentsByName = getOperationResult.fragmentsByName; - OperationDefinition operationDefinition = getOperationResult.operationDefinition; - - RawVariables inputVariables = executionInput.getRawVariables(); - List variableDefinitions = operationDefinition.getVariableDefinitions(); - - CoercedVariables coercedVariables = ValuesResolver.coerceVariableValues(graphQLSchema, variableDefinitions, inputVariables); - - ExecutionContext executionContext = newExecutionContextBuilder() - .executionId(executionId) - .instrumentationState(instrumentationState) - .graphQLSchema(graphQLSchema) - .context(executionInput.getContext()) - .graphQLContext(executionInput.getGraphQLContext()) - .root(executionInput.getRoot()) - .fragmentsByName(fragmentsByName) - .coercedVariables(coercedVariables) - .document(document) - .operationDefinition(operationDefinition) - .build(); - - ExecutionData executionData = new ExecutionData(); - executionData.executionContext = executionContext; - return executionData; - } - - public FieldSubSelection getFieldSubSelection(ExecutionContext executionContext) { - OperationDefinition operationDefinition = executionContext.getOperationDefinition(); - GraphQLObjectType operationRootType = Common.getOperationRootType(executionContext.getGraphQLSchema(), operationDefinition); - - FieldCollectorParameters collectorParameters = FieldCollectorParameters.newParameters() - .schema(executionContext.getGraphQLSchema()) - .objectType(operationRootType) - .fragments(executionContext.getFragmentsByName()) - .variables(executionContext.getVariables()) - .build(); - - MergedSelectionSet mergedSelectionSet = fieldCollector.collectFields(collectorParameters, operationDefinition.getSelectionSet()); - ExecutionStepInfo executionInfo = newExecutionStepInfo().type(operationRootType).path(ResultPath.rootPath()).build(); - - FieldSubSelection fieldSubSelection = FieldSubSelection.newFieldSubSelection() - .source(executionContext.getRoot()) - .localContext(executionContext.getLocalContext()) - .mergedSelectionSet(mergedSelectionSet) - .executionInfo(executionInfo) - .build(); - return fieldSubSelection; - } -} diff --git a/src/main/java/graphql/execution/nextgen/ExecutionStrategy.java b/src/main/java/graphql/execution/nextgen/ExecutionStrategy.java deleted file mode 100644 index 70534d66d8..0000000000 --- a/src/main/java/graphql/execution/nextgen/ExecutionStrategy.java +++ /dev/null @@ -1,18 +0,0 @@ -package graphql.execution.nextgen; - -import graphql.ExecutionResult; -import graphql.Internal; -import graphql.execution.ExecutionContext; - -import java.util.concurrent.CompletableFuture; - -/** - * @deprecated Jan 2022 - We have decided to deprecate the NextGen engine, and it will be removed in a future release. - */ -@Deprecated -@Internal -public interface ExecutionStrategy { - - CompletableFuture execute(ExecutionContext context); - -} diff --git a/src/main/java/graphql/execution/nextgen/ExecutionStrategyUtil.java b/src/main/java/graphql/execution/nextgen/ExecutionStrategyUtil.java deleted file mode 100644 index 51c2e058a0..0000000000 --- a/src/main/java/graphql/execution/nextgen/ExecutionStrategyUtil.java +++ /dev/null @@ -1,102 +0,0 @@ -package graphql.execution.nextgen; - -import graphql.Internal; -import graphql.execution.Async; -import graphql.execution.ExecutionContext; -import graphql.execution.ExecutionStepInfo; -import graphql.execution.ExecutionStepInfoFactory; -import graphql.execution.FetchedValue; -import graphql.execution.FieldCollector; -import graphql.execution.FieldCollectorParameters; -import graphql.execution.MergedField; -import graphql.execution.MergedSelectionSet; -import graphql.execution.ResolveType; -import graphql.execution.nextgen.result.ExecutionResultNode; -import graphql.execution.nextgen.result.ResolvedValue; -import graphql.schema.GraphQLObjectType; -import graphql.schema.GraphQLOutputType; - -import java.util.List; -import java.util.concurrent.CompletableFuture; - -import static graphql.collect.ImmutableKit.map; -import static graphql.execution.FieldCollectorParameters.newParameters; - -/** - * @deprecated Jan 2022 - We have decided to deprecate the NextGen engine, and it will be removed in a future release. - */ -@Deprecated -@Internal -public class ExecutionStrategyUtil { - - ExecutionStepInfoFactory executionStepInfoFactory = new ExecutionStepInfoFactory(); - FetchedValueAnalyzer fetchedValueAnalyzer = new FetchedValueAnalyzer(); - ValueFetcher valueFetcher = new ValueFetcher(); - ResultNodesCreator resultNodesCreator = new ResultNodesCreator(); - ResolveType resolveType = new ResolveType(); - FieldCollector fieldCollector = new FieldCollector(); - - public List> fetchSubSelection(ExecutionContext executionContext, FieldSubSelection fieldSubSelection) { - List> fetchedValueAnalysisList = fetchAndAnalyze(executionContext, fieldSubSelection); - return fetchedValueAnalysisToNodesAsync(fetchedValueAnalysisList); - } - - private List> fetchAndAnalyze(ExecutionContext context, FieldSubSelection fieldSubSelection) { - - return map(fieldSubSelection.getMergedSelectionSet().getSubFieldsList(), - mergedField -> fetchAndAnalyzeField(context, fieldSubSelection.getSource(), fieldSubSelection.getLocalContext(), mergedField, fieldSubSelection.getExecutionStepInfo())); - - } - - private CompletableFuture fetchAndAnalyzeField(ExecutionContext context, Object source, Object localContext, MergedField mergedField, - ExecutionStepInfo executionStepInfo) { - - ExecutionStepInfo newExecutionStepInfo = executionStepInfoFactory.newExecutionStepInfoForSubField(context, mergedField, executionStepInfo); - return valueFetcher - .fetchValue(context, source, localContext, mergedField, newExecutionStepInfo) - .thenApply(fetchValue -> analyseValue(context, fetchValue, newExecutionStepInfo)); - } - - private List> fetchedValueAnalysisToNodesAsync(List> list) { - return Async.map(list, fetchedValueAnalysis -> resultNodesCreator.createResultNode(fetchedValueAnalysis)); - } - - public List fetchedValueAnalysisToNodes(List fetchedValueAnalysisList) { - return map(fetchedValueAnalysisList, fetchedValueAnalysis -> resultNodesCreator.createResultNode(fetchedValueAnalysis)); - } - - - private FetchedValueAnalysis analyseValue(ExecutionContext executionContext, FetchedValue fetchedValue, ExecutionStepInfo executionInfo) { - FetchedValueAnalysis fetchedValueAnalysis = fetchedValueAnalyzer.analyzeFetchedValue(executionContext, fetchedValue, executionInfo); - return fetchedValueAnalysis; - } - - public FieldSubSelection createFieldSubSelection(ExecutionContext executionContext, ExecutionStepInfo executionInfo, ResolvedValue resolvedValue) { - MergedField field = executionInfo.getField(); - Object source = resolvedValue.getCompletedValue(); - Object localContext = resolvedValue.getLocalContext(); - - GraphQLOutputType sourceType = executionInfo.getUnwrappedNonNullType(); - GraphQLObjectType resolvedObjectType = resolveType.resolveType(executionContext, field, source, executionInfo, sourceType, localContext); - FieldCollectorParameters collectorParameters = newParameters() - .schema(executionContext.getGraphQLSchema()) - .objectType(resolvedObjectType) - .fragments(executionContext.getFragmentsByName()) - .variables(executionContext.getVariables()) - .build(); - MergedSelectionSet subFields = fieldCollector.collectFields(collectorParameters, - executionInfo.getField()); - - // it is not really a new step but rather a refinement - ExecutionStepInfo newExecutionStepInfoWithResolvedType = executionInfo.changeTypeWithPreservedNonNull(resolvedObjectType); - - return FieldSubSelection.newFieldSubSelection() - .source(source) - .localContext(localContext) - .mergedSelectionSet(subFields) - .executionInfo(newExecutionStepInfoWithResolvedType) - .build(); - } - - -} diff --git a/src/main/java/graphql/execution/nextgen/FetchedValueAnalysis.java b/src/main/java/graphql/execution/nextgen/FetchedValueAnalysis.java deleted file mode 100644 index 098da68118..0000000000 --- a/src/main/java/graphql/execution/nextgen/FetchedValueAnalysis.java +++ /dev/null @@ -1,203 +0,0 @@ -package graphql.execution.nextgen; - -import graphql.GraphQLError; -import graphql.Internal; -import graphql.execution.ExecutionStepInfo; -import graphql.execution.FetchedValue; -import graphql.execution.MergedField; -import graphql.schema.GraphQLObjectType; - -import java.util.ArrayList; -import java.util.List; -import java.util.function.Consumer; - -import static graphql.Assert.assertNotNull; - -/** - * @deprecated Jan 2022 - We have decided to deprecate the NextGen engine, and it will be removed in a future release. - */ -@Deprecated -@Internal -public class FetchedValueAnalysis { - - public enum FetchedValueType { - OBJECT, - LIST, - SCALAR, - ENUM, - } - - private final FetchedValueType valueType; - private final List errors; - // not applicable for LIST - private final Object completedValue; - private final boolean nullValue; - // only available for LIST - private final List children; - // only for object - private final GraphQLObjectType resolvedType; - private final ExecutionStepInfo executionStepInfo; - // for LIST this is the whole list - private final FetchedValue fetchedValue; - - private FetchedValueAnalysis(Builder builder) { - this.errors = new ArrayList<>(builder.errors); - this.errors.addAll(builder.fetchedValue.getErrors()); - this.valueType = assertNotNull(builder.valueType); - this.completedValue = builder.completedValue; - this.nullValue = builder.nullValue; - this.children = builder.children; - this.resolvedType = builder.resolvedType; - this.executionStepInfo = assertNotNull(builder.executionInfo); - this.fetchedValue = assertNotNull(builder.fetchedValue); - } - - public FetchedValueType getValueType() { - return valueType; - } - - public List getErrors() { - return errors; - } - - public Object getCompletedValue() { - return completedValue; - } - - public List getChildren() { - return children; - } - - public boolean isNullValue() { - return nullValue; - } - - public FetchedValue getFetchedValue() { - return fetchedValue; - } - - public FetchedValueAnalysis transform(Consumer builderConsumer) { - Builder builder = new Builder(this); - builderConsumer.accept(builder); - return builder.build(); - } - - public static Builder newFetchedValueAnalysis() { - return new Builder(); - } - - public static Builder newFetchedValueAnalysis(FetchedValueType valueType) { - return new Builder().valueType(valueType); - } - - public static Builder newFetchedValueAnalysis(FetchedValueAnalysis existing) { - return new Builder(existing); - } - - public ExecutionStepInfo getExecutionStepInfo() { - return executionStepInfo; - } - - public GraphQLObjectType getResolvedType() { - return resolvedType; - } - - public MergedField getField() { - return executionStepInfo.getField(); - } - - public String getResultKey() { - return executionStepInfo.getResultKey(); - } - - @Override - public String toString() { - return "{" + - "valueType=" + valueType + - ", completedValue=" + completedValue + - ", errors=" + errors + - ", children=" + children + - ", stepInfo=" + executionStepInfo + - ", nullValue=" + nullValue + - ", resolvedType=" + resolvedType + - ", fetchedValue=" + fetchedValue + - '}'; - } - - public static final class Builder { - private FetchedValueType valueType; - private final List errors = new ArrayList<>(); - private Object completedValue; - private FetchedValue fetchedValue; - private List children; - private GraphQLObjectType resolvedType; - private boolean nullValue; - private ExecutionStepInfo executionInfo; - - private Builder() { - } - - private Builder(FetchedValueAnalysis existing) { - valueType = existing.getValueType(); - errors.addAll(existing.getErrors()); - completedValue = existing.getCompletedValue(); - fetchedValue = existing.getFetchedValue(); - children = existing.getChildren(); - nullValue = existing.isNullValue(); - resolvedType = existing.getResolvedType(); - executionInfo = existing.getExecutionStepInfo(); - } - - - public Builder valueType(FetchedValueType val) { - valueType = val; - return this; - } - - public Builder errors(List errors) { - this.errors.addAll(errors); - return this; - } - - public Builder error(GraphQLError error) { - this.errors.add(error); - return this; - } - - - public Builder completedValue(Object completedValue) { - this.completedValue = completedValue; - return this; - } - - public Builder children(List children) { - this.children = children; - return this; - } - - - public Builder nullValue() { - this.nullValue = true; - return this; - } - - public Builder resolvedType(GraphQLObjectType resolvedType) { - this.resolvedType = resolvedType; - return this; - } - - public Builder executionStepInfo(ExecutionStepInfo executionInfo) { - this.executionInfo = executionInfo; - return this; - } - - public Builder fetchedValue(FetchedValue fetchedValue) { - this.fetchedValue = fetchedValue; - return this; - } - - public FetchedValueAnalysis build() { - return new FetchedValueAnalysis(this); - } - } -} diff --git a/src/main/java/graphql/execution/nextgen/FetchedValueAnalyzer.java b/src/main/java/graphql/execution/nextgen/FetchedValueAnalyzer.java deleted file mode 100644 index 1b93889972..0000000000 --- a/src/main/java/graphql/execution/nextgen/FetchedValueAnalyzer.java +++ /dev/null @@ -1,214 +0,0 @@ -package graphql.execution.nextgen; - -import graphql.Internal; -import graphql.SerializationError; -import graphql.TypeMismatchError; -import graphql.UnresolvedTypeError; -import graphql.execution.ExecutionContext; -import graphql.execution.ExecutionStepInfo; -import graphql.execution.ExecutionStepInfoFactory; -import graphql.execution.FetchedValue; -import graphql.execution.MergedField; -import graphql.execution.NonNullableFieldWasNullException; -import graphql.execution.ResolveType; -import graphql.execution.UnresolvedTypeException; -import graphql.schema.CoercingSerializeException; -import graphql.schema.GraphQLEnumType; -import graphql.schema.GraphQLObjectType; -import graphql.schema.GraphQLScalarType; -import graphql.schema.GraphQLType; -import graphql.util.FpKit; - -import java.util.ArrayList; -import java.util.Collection; -import java.util.List; - -import static graphql.execution.nextgen.FetchedValueAnalysis.FetchedValueType.ENUM; -import static graphql.execution.nextgen.FetchedValueAnalysis.FetchedValueType.LIST; -import static graphql.execution.nextgen.FetchedValueAnalysis.FetchedValueType.OBJECT; -import static graphql.execution.nextgen.FetchedValueAnalysis.FetchedValueType.SCALAR; -import static graphql.execution.nextgen.FetchedValueAnalysis.newFetchedValueAnalysis; -import static graphql.schema.GraphQLTypeUtil.isList; - -/** - * @deprecated Jan 2022 - We have decided to deprecate the NextGen engine, and it will be removed in a future release. - */ -@Deprecated -@Internal -public class FetchedValueAnalyzer { - - ExecutionStepInfoFactory executionInfoFactory = new ExecutionStepInfoFactory(); - ResolveType resolveType = new ResolveType(); - - - /* - * scalar: the value, null and/or error - * enum: same as scalar - * list: list of X: X can be list again, list of scalars or enum or objects - */ - public FetchedValueAnalysis analyzeFetchedValue(ExecutionContext executionContext, FetchedValue fetchedValue, ExecutionStepInfo executionInfo) throws NonNullableFieldWasNullException { - return analyzeFetchedValueImpl(executionContext, fetchedValue, fetchedValue.getFetchedValue(), executionInfo); - } - - private FetchedValueAnalysis analyzeFetchedValueImpl(ExecutionContext executionContext, FetchedValue fetchedValue, Object toAnalyze, ExecutionStepInfo executionInfo) throws NonNullableFieldWasNullException { - GraphQLType fieldType = executionInfo.getUnwrappedNonNullType(); - MergedField field = executionInfo.getField(); - - if (isList(fieldType)) { - return analyzeList(executionContext, fetchedValue, toAnalyze, executionInfo); - } else if (fieldType instanceof GraphQLScalarType) { - return analyzeScalarValue(fetchedValue, toAnalyze, (GraphQLScalarType) fieldType, executionInfo); - } else if (fieldType instanceof GraphQLEnumType) { - return analyzeEnumValue(fetchedValue, toAnalyze, (GraphQLEnumType) fieldType, executionInfo); - } - - // when we are here, we have a complex type: Interface, Union or Object - // and we must go deeper - // - if (toAnalyze == null) { - return newFetchedValueAnalysis(OBJECT) - .fetchedValue(fetchedValue) - .executionStepInfo(executionInfo) - .nullValue() - .build(); - } - try { - GraphQLObjectType resolvedObjectType = resolveType.resolveType(executionContext, field, toAnalyze, executionInfo, fieldType, fetchedValue.getLocalContext()); - return newFetchedValueAnalysis(OBJECT) - .fetchedValue(fetchedValue) - .executionStepInfo(executionInfo) - .completedValue(toAnalyze) - .resolvedType(resolvedObjectType) - .build(); - } catch (UnresolvedTypeException ex) { - return handleUnresolvedTypeProblem(fetchedValue, executionInfo, ex); - } - } - - - private FetchedValueAnalysis handleUnresolvedTypeProblem(FetchedValue fetchedValue, ExecutionStepInfo executionInfo, UnresolvedTypeException e) { - UnresolvedTypeError error = new UnresolvedTypeError(executionInfo.getPath(), executionInfo, e); - return newFetchedValueAnalysis(OBJECT) - .fetchedValue(fetchedValue) - .executionStepInfo(executionInfo) - .nullValue() - .error(error) - .build(); - } - - private FetchedValueAnalysis analyzeList(ExecutionContext executionContext, FetchedValue fetchedValue, Object toAnalyze, ExecutionStepInfo executionInfo) { - if (toAnalyze == null) { - return newFetchedValueAnalysis(LIST) - .fetchedValue(fetchedValue) - .executionStepInfo(executionInfo) - .nullValue() - .build(); - } - - if (toAnalyze.getClass().isArray() || toAnalyze instanceof Iterable) { - Collection collection = FpKit.toCollection(toAnalyze); - return analyzeIterable(executionContext, fetchedValue, collection, executionInfo); - } else { - TypeMismatchError error = new TypeMismatchError(executionInfo.getPath(), executionInfo.getType()); - return newFetchedValueAnalysis(LIST) - .fetchedValue(fetchedValue) - .executionStepInfo(executionInfo) - .nullValue() - .error(error) - .build(); - } - - } - - private FetchedValueAnalysis analyzeIterable(ExecutionContext executionContext, FetchedValue fetchedValue, Iterable iterableValues, ExecutionStepInfo executionInfo) { - - Collection values = FpKit.toCollection(iterableValues); - List children = new ArrayList<>(); - int index = 0; - for (Object item : values) { - ExecutionStepInfo executionInfoForListElement = executionInfoFactory.newExecutionStepInfoForListElement(executionInfo, index); - children.add(analyzeFetchedValueImpl(executionContext, fetchedValue, item, executionInfoForListElement)); - index++; - } - return newFetchedValueAnalysis(LIST) - .fetchedValue(fetchedValue) - .executionStepInfo(executionInfo) - .children(children) - .build(); - - } - - - private FetchedValueAnalysis analyzeScalarValue(FetchedValue fetchedValue, Object toAnalyze, GraphQLScalarType scalarType, ExecutionStepInfo executionInfo) { - if (toAnalyze == null) { - return newFetchedValueAnalysis(SCALAR) - .fetchedValue(fetchedValue) - .executionStepInfo(executionInfo) - .nullValue() - .build(); - } - Object serialized; - try { - serialized = serializeScalarValue(toAnalyze, scalarType); - } catch (CoercingSerializeException e) { - SerializationError error = new SerializationError(executionInfo.getPath(), e); - return newFetchedValueAnalysis(SCALAR) - .fetchedValue(fetchedValue) - .executionStepInfo(executionInfo) - .error(error) - .nullValue() - .build(); - } - - // TODO: fix that: this should not be handled here - //6.6.1 http://facebook.github.io/graphql/#sec-Field-entries - if (serialized instanceof Double && ((Double) serialized).isNaN()) { - return newFetchedValueAnalysis(SCALAR) - .fetchedValue(fetchedValue) - .executionStepInfo(executionInfo) - .nullValue() - .build(); - } - // handle non null - - return newFetchedValueAnalysis(SCALAR) - .fetchedValue(fetchedValue) - .executionStepInfo(executionInfo) - .completedValue(serialized) - .build(); - } - - protected Object serializeScalarValue(Object toAnalyze, GraphQLScalarType scalarType) throws CoercingSerializeException { - return scalarType.getCoercing().serialize(toAnalyze); - } - - private FetchedValueAnalysis analyzeEnumValue(FetchedValue fetchedValue, Object toAnalyze, GraphQLEnumType enumType, ExecutionStepInfo executionInfo) { - if (toAnalyze == null) { - return newFetchedValueAnalysis(SCALAR) - .fetchedValue(fetchedValue) - .executionStepInfo(executionInfo) - .nullValue() - .build(); - - } - Object serialized; - try { - serialized = enumType.serialize(toAnalyze); - } catch (CoercingSerializeException e) { - SerializationError error = new SerializationError(executionInfo.getPath(), e); - return newFetchedValueAnalysis(SCALAR) - .fetchedValue(fetchedValue) - .executionStepInfo(executionInfo) - .nullValue() - .error(error) - .build(); - } - // handle non null values - return newFetchedValueAnalysis(ENUM) - .fetchedValue(fetchedValue) - .executionStepInfo(executionInfo) - .completedValue(serialized) - .build(); - } - -} diff --git a/src/main/java/graphql/execution/nextgen/FieldSubSelection.java b/src/main/java/graphql/execution/nextgen/FieldSubSelection.java deleted file mode 100644 index ac3407a4a1..0000000000 --- a/src/main/java/graphql/execution/nextgen/FieldSubSelection.java +++ /dev/null @@ -1,100 +0,0 @@ -package graphql.execution.nextgen; - -import graphql.Internal; -import graphql.execution.ExecutionStepInfo; -import graphql.execution.MergedField; -import graphql.execution.MergedSelectionSet; - -import java.util.Map; - - -/** - * A map from name to List of Field representing the actual sub selections (during execution) of a Field with Fragments - * evaluated and conditional directives considered. - * - * @deprecated Jan 2022 - We have decided to deprecate the NextGen engine, and it will be removed in a future release. - */ -@Deprecated -@Internal -public class FieldSubSelection { - - private final Object source; - private final Object localContext; - // the type of this must be objectType and is the parent executionStepInfo for all mergedSelectionSet - private final ExecutionStepInfo executionInfo; - private final MergedSelectionSet mergedSelectionSet; - - private FieldSubSelection(Builder builder) { - this.source = builder.source; - this.localContext = builder.localContext; - this.executionInfo = builder.executionInfo; - this.mergedSelectionSet = builder.mergedSelectionSet; - } - - public Object getSource() { - return source; - } - - public Object getLocalContext() { - return localContext; - } - - public Map getSubFields() { - return mergedSelectionSet.getSubFields(); - } - - public MergedSelectionSet getMergedSelectionSet() { - return mergedSelectionSet; - } - - public ExecutionStepInfo getExecutionStepInfo() { - return executionInfo; - } - - @Override - public String toString() { - return "FieldSubSelection{" + - "source=" + source + - ", executionInfo=" + executionInfo + - ", mergedSelectionSet" + mergedSelectionSet + - '}'; - } - - public static Builder newFieldSubSelection() { - return new Builder(); - } - - public static class Builder { - private Object source; - private Object localContext; - private ExecutionStepInfo executionInfo; - private MergedSelectionSet mergedSelectionSet; - - public Builder source(Object source) { - this.source = source; - return this; - } - - public Builder localContext(Object localContext) { - this.localContext = localContext; - return this; - } - - public Builder executionInfo(ExecutionStepInfo executionInfo) { - this.executionInfo = executionInfo; - return this; - } - - public Builder mergedSelectionSet(MergedSelectionSet mergedSelectionSet) { - this.mergedSelectionSet = mergedSelectionSet; - return this; - } - - public FieldSubSelection build() { - return new FieldSubSelection(this); - } - - - } - -} diff --git a/src/main/java/graphql/execution/nextgen/ResultNodesCreator.java b/src/main/java/graphql/execution/nextgen/ResultNodesCreator.java deleted file mode 100644 index 4366f875d0..0000000000 --- a/src/main/java/graphql/execution/nextgen/ResultNodesCreator.java +++ /dev/null @@ -1,70 +0,0 @@ -package graphql.execution.nextgen; - -import graphql.Internal; -import graphql.execution.ExecutionStepInfo; -import graphql.execution.NonNullableFieldWasNullException; -import graphql.execution.nextgen.result.ExecutionResultNode; -import graphql.execution.nextgen.result.LeafExecutionResultNode; -import graphql.execution.nextgen.result.ListExecutionResultNode; -import graphql.execution.nextgen.result.ResolvedValue; -import graphql.execution.nextgen.result.UnresolvedObjectResultNode; - -import java.util.Collection; -import java.util.List; -import java.util.Optional; - -import static graphql.collect.ImmutableKit.map; - -/** - * @deprecated Jan 2022 - We have decided to deprecate the NextGen engine, and it will be removed in a future release. - */ -@Deprecated -@Internal -public class ResultNodesCreator { - - public ExecutionResultNode createResultNode(FetchedValueAnalysis fetchedValueAnalysis) { - ResolvedValue resolvedValue = createResolvedValue(fetchedValueAnalysis); - ExecutionStepInfo executionStepInfo = fetchedValueAnalysis.getExecutionStepInfo(); - - if (fetchedValueAnalysis.isNullValue() && executionStepInfo.isNonNullType()) { - NonNullableFieldWasNullException nonNullableFieldWasNullException = new NonNullableFieldWasNullException(executionStepInfo, executionStepInfo.getPath()); - - return new LeafExecutionResultNode(executionStepInfo, resolvedValue, nonNullableFieldWasNullException); - } - if (fetchedValueAnalysis.isNullValue()) { - return new LeafExecutionResultNode(executionStepInfo, resolvedValue, null); - } - if (fetchedValueAnalysis.getValueType() == FetchedValueAnalysis.FetchedValueType.OBJECT) { - return createUnresolvedNode(fetchedValueAnalysis); - } - if (fetchedValueAnalysis.getValueType() == FetchedValueAnalysis.FetchedValueType.LIST) { - return createListResultNode(fetchedValueAnalysis); - } - return new LeafExecutionResultNode(executionStepInfo, resolvedValue, null); - } - - private ExecutionResultNode createUnresolvedNode(FetchedValueAnalysis fetchedValueAnalysis) { - return new UnresolvedObjectResultNode(fetchedValueAnalysis.getExecutionStepInfo(), createResolvedValue(fetchedValueAnalysis)); - } - - private ResolvedValue createResolvedValue(FetchedValueAnalysis fetchedValueAnalysis) { - return ResolvedValue.newResolvedValue() - .completedValue(fetchedValueAnalysis.getCompletedValue()) - .localContext(fetchedValueAnalysis.getFetchedValue().getLocalContext()) - .nullValue(fetchedValueAnalysis.isNullValue()) - .errors(fetchedValueAnalysis.getErrors()) - .build(); - } - - private Optional getFirstNonNullableException(Collection collection) { - return collection.stream() - .filter(executionResultNode -> executionResultNode.getNonNullableFieldWasNullException() != null) - .map(ExecutionResultNode::getNonNullableFieldWasNullException) - .findFirst(); - } - - private ExecutionResultNode createListResultNode(FetchedValueAnalysis fetchedValueAnalysis) { - List executionResultNodes = map(fetchedValueAnalysis.getChildren(), this::createResultNode); - return new ListExecutionResultNode(fetchedValueAnalysis.getExecutionStepInfo(), createResolvedValue(fetchedValueAnalysis), executionResultNodes); - } -} diff --git a/src/main/java/graphql/execution/nextgen/ValueFetcher.java b/src/main/java/graphql/execution/nextgen/ValueFetcher.java deleted file mode 100644 index 0eddc24274..0000000000 --- a/src/main/java/graphql/execution/nextgen/ValueFetcher.java +++ /dev/null @@ -1,230 +0,0 @@ -package graphql.execution.nextgen; - - -import com.google.common.collect.ImmutableList; -import graphql.Assert; -import graphql.ExceptionWhileDataFetching; -import graphql.GraphQLError; -import graphql.Internal; -import graphql.collect.ImmutableKit; -import graphql.execution.Async; -import graphql.execution.DataFetcherResult; -import graphql.execution.DefaultValueUnboxer; -import graphql.execution.ExecutionContext; -import graphql.execution.ExecutionId; -import graphql.execution.ExecutionStepInfo; -import graphql.execution.FetchedValue; -import graphql.execution.MergedField; -import graphql.execution.ResultPath; -import graphql.execution.ValuesResolver; -import graphql.execution.directives.QueryDirectivesImpl; -import graphql.language.Field; -import graphql.normalized.ExecutableNormalizedField; -import graphql.normalized.ExecutableNormalizedOperation; -import graphql.schema.DataFetcher; -import graphql.schema.DataFetchingEnvironment; -import graphql.schema.DataFetchingFieldSelectionSet; -import graphql.schema.DataFetchingFieldSelectionSetImpl; -import graphql.schema.GraphQLCodeRegistry; -import graphql.schema.GraphQLFieldDefinition; -import graphql.schema.GraphQLFieldsContainer; -import graphql.schema.GraphQLOutputType; -import graphql.schema.GraphQLTypeUtil; -import graphql.util.FpKit; -import graphql.util.LogKit; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -import java.util.ArrayList; -import java.util.List; -import java.util.Map; -import java.util.concurrent.CompletableFuture; -import java.util.concurrent.CompletionStage; -import java.util.function.Supplier; - -import static graphql.schema.DataFetchingEnvironmentImpl.newDataFetchingEnvironment; -import static java.util.Collections.singletonList; - -/** - * @deprecated Jan 2022 - We have decided to deprecate the NextGen engine, and it will be removed in a future release. - */ -@Deprecated -@Internal -public class ValueFetcher { - private static final Logger log = LoggerFactory.getLogger(ValueFetcher.class); - private static final Logger logNotSafe = LogKit.getNotPrivacySafeLogger(ExecutionStrategy.class); - - public static final Object NULL_VALUE = new Object(); - - public ValueFetcher() { - } - - - public CompletableFuture> fetchBatchedValues(ExecutionContext executionContext, List sources, MergedField field, List executionInfos) { - ExecutionStepInfo executionStepInfo = executionInfos.get(0); - // TODO - add support for field context to batching code - Object todoLocalContext = null; - if (isDataFetcherBatched(executionContext, executionStepInfo)) { - return fetchValue(executionContext, sources, todoLocalContext, field, executionStepInfo) - .thenApply(fetchedValue -> extractBatchedValues(fetchedValue, sources.size())); - } else { - List> fetchedValues = new ArrayList<>(); - for (int i = 0; i < sources.size(); i++) { - fetchedValues.add(fetchValue(executionContext, sources.get(i), todoLocalContext, field, executionInfos.get(i))); - } - return Async.each(fetchedValues); - } - } - - @SuppressWarnings("unchecked") - private List extractBatchedValues(FetchedValue fetchedValueContainingList, int expectedSize) { - List list = (List) fetchedValueContainingList.getFetchedValue(); - Assert.assertTrue(list.size() == expectedSize, () -> "Unexpected result size"); - List result = new ArrayList<>(); - for (int i = 0; i < list.size(); i++) { - List errors; - if (i == 0) { - errors = fetchedValueContainingList.getErrors(); - } else { - errors = ImmutableKit.emptyList(); - } - FetchedValue fetchedValue = FetchedValue.newFetchedValue() - .fetchedValue(list.get(i)) - .rawFetchedValue(fetchedValueContainingList.getRawFetchedValue()) - .errors(errors) - .localContext(fetchedValueContainingList.getLocalContext()) - .build(); - result.add(fetchedValue); - } - return result; - } - - private GraphQLFieldsContainer getFieldsContainer(ExecutionStepInfo executionStepInfo) { - GraphQLOutputType type = executionStepInfo.getParent().getType(); - return (GraphQLFieldsContainer) GraphQLTypeUtil.unwrapAll(type); - } - - private boolean isDataFetcherBatched(ExecutionContext executionContext, ExecutionStepInfo executionStepInfo) { - GraphQLFieldsContainer parentType = getFieldsContainer(executionStepInfo); - GraphQLFieldDefinition fieldDef = executionStepInfo.getFieldDefinition(); - DataFetcher dataFetcher = executionContext.getGraphQLSchema().getCodeRegistry().getDataFetcher(parentType, fieldDef); - return dataFetcher instanceof BatchedDataFetcher; - } - - public CompletableFuture fetchValue(ExecutionContext executionContext, Object source, Object localContext, MergedField sameFields, ExecutionStepInfo executionInfo) { - Field field = sameFields.getSingleField(); - GraphQLFieldDefinition fieldDef = executionInfo.getFieldDefinition(); - - GraphQLCodeRegistry codeRegistry = executionContext.getGraphQLSchema().getCodeRegistry(); - GraphQLFieldsContainer parentType = getFieldsContainer(executionInfo); - - Supplier> argumentValues = FpKit.intraThreadMemoize(() -> ValuesResolver.getArgumentValues(codeRegistry, fieldDef.getArguments(), field.getArguments(), executionContext.getCoercedVariables())); - - QueryDirectivesImpl queryDirectives = new QueryDirectivesImpl(sameFields, executionContext.getGraphQLSchema(), executionContext.getVariables()); - - GraphQLOutputType fieldType = fieldDef.getType(); - - Supplier normalizedQuery = executionContext.getNormalizedQueryTree(); - Supplier normalisedField = () -> normalizedQuery.get().getNormalizedField(sameFields, executionInfo.getObjectType(), executionInfo.getPath()); - DataFetchingFieldSelectionSet selectionSet = DataFetchingFieldSelectionSetImpl.newCollector(executionContext.getGraphQLSchema(), fieldType, normalisedField); - - DataFetchingEnvironment environment = newDataFetchingEnvironment(executionContext) - .source(source) - .localContext(localContext) - .arguments(argumentValues) - .fieldDefinition(fieldDef) - .mergedField(sameFields) - .fieldType(fieldType) - .executionStepInfo(executionInfo) - .parentType(parentType) - .selectionSet(selectionSet) - .queryDirectives(queryDirectives) - .build(); - - ExecutionId executionId = executionContext.getExecutionId(); - ResultPath path = executionInfo.getPath(); - return callDataFetcher(codeRegistry, parentType, fieldDef, environment, executionId, path) - .thenApply(rawFetchedValue -> FetchedValue.newFetchedValue() - .fetchedValue(rawFetchedValue) - .rawFetchedValue(rawFetchedValue) - .build()) - .exceptionally(exception -> handleExceptionWhileFetching(field, path, exception)) - .thenApply(result -> unboxPossibleDataFetcherResult(sameFields, path, result, localContext)) - .thenApply(this::unboxPossibleOptional); - } - - private FetchedValue handleExceptionWhileFetching(Field field, ResultPath path, Throwable exception) { - ExceptionWhileDataFetching exceptionWhileDataFetching = new ExceptionWhileDataFetching(path, exception, field.getSourceLocation()); - return FetchedValue.newFetchedValue().errors(singletonList(exceptionWhileDataFetching)).build(); - } - - private FetchedValue unboxPossibleOptional(FetchedValue result) { - return result.transform( - builder -> builder.fetchedValue(DefaultValueUnboxer.unboxValue(result.getFetchedValue())) - ); - } - - private CompletableFuture callDataFetcher(GraphQLCodeRegistry codeRegistry, GraphQLFieldsContainer parentType, GraphQLFieldDefinition fieldDef, DataFetchingEnvironment environment, ExecutionId executionId, ResultPath path) { - CompletableFuture result = new CompletableFuture<>(); - try { - DataFetcher dataFetcher = codeRegistry.getDataFetcher(parentType, fieldDef); - if (log.isDebugEnabled()) { - log.debug("'{}' fetching field '{}' using data fetcher '{}'...", executionId, path, dataFetcher.getClass().getName()); - } - Object fetchedValueRaw = dataFetcher.get(environment); - if (logNotSafe.isDebugEnabled()) { - logNotSafe.debug("'{}' field '{}' fetch returned '{}'", executionId, path, fetchedValueRaw == null ? "null" : fetchedValueRaw.getClass().getName()); - } - handleFetchedValue(fetchedValueRaw, result); - } catch (Exception e) { - if (logNotSafe.isDebugEnabled()) { - logNotSafe.debug("'{}', field '{}' fetch threw exception", executionId, path, e); - } - result.completeExceptionally(e); - } - return result; - } - - private void handleFetchedValue(Object fetchedValue, CompletableFuture cf) { - if (fetchedValue == null) { - cf.complete(NULL_VALUE); - return; - } - if (fetchedValue instanceof CompletionStage) { - //noinspection unchecked - CompletionStage stage = (CompletionStage) fetchedValue; - stage.whenComplete((value, throwable) -> { - if (throwable != null) { - cf.completeExceptionally(throwable); - } else { - cf.complete(value); - } - }); - return; - } - cf.complete(fetchedValue); - } - - private FetchedValue unboxPossibleDataFetcherResult(MergedField sameField, ResultPath resultPath, FetchedValue result, Object localContext) { - if (result.getFetchedValue() instanceof DataFetcherResult) { - - DataFetcherResult dataFetcherResult = (DataFetcherResult) result.getFetchedValue(); - List addErrors = ImmutableList.copyOf(dataFetcherResult.getErrors()); - List newErrors = ImmutableKit.concatLists(result.getErrors(), addErrors); - - Object newLocalContext = dataFetcherResult.getLocalContext(); - if (newLocalContext == null) { - // if the field returns nothing then they get the context of their parent field - newLocalContext = localContext; - } - return FetchedValue.newFetchedValue() - .fetchedValue(dataFetcherResult.getData()) - .rawFetchedValue(result.getRawFetchedValue()) - .errors(newErrors) - .localContext(newLocalContext) - .build(); - } else { - return result; - } - } -} diff --git a/src/main/java/graphql/execution/nextgen/package-info.java b/src/main/java/graphql/execution/nextgen/package-info.java deleted file mode 100644 index 703901ee64..0000000000 --- a/src/main/java/graphql/execution/nextgen/package-info.java +++ /dev/null @@ -1,11 +0,0 @@ -/** - * WARNING: All code in this package is a work in progress for a new execution engine. - * It is not really "wired up" and can't be really used and should not be used yet! - * - * Jan 2022 - We have decided to deprecate the NextGen engine, and it will be removed in a future release. - */ -@Internal -@Deprecated -package graphql.execution.nextgen; - -import graphql.Internal; \ No newline at end of file diff --git a/src/main/java/graphql/execution/nextgen/result/ExecutionResultNode.java b/src/main/java/graphql/execution/nextgen/result/ExecutionResultNode.java deleted file mode 100644 index 907112a707..0000000000 --- a/src/main/java/graphql/execution/nextgen/result/ExecutionResultNode.java +++ /dev/null @@ -1,115 +0,0 @@ -package graphql.execution.nextgen.result; - -import com.google.common.collect.ImmutableList; -import graphql.Assert; -import graphql.GraphQLError; -import graphql.Internal; -import graphql.execution.ExecutionStepInfo; -import graphql.execution.MergedField; -import graphql.execution.NonNullableFieldWasNullException; - -import java.util.ArrayList; -import java.util.Collections; -import java.util.List; -import java.util.Optional; - -import static graphql.Assert.assertNotNull; - -/** - * @deprecated Jan 2022 - We have decided to deprecate the NextGen engine, and it will be removed in a future release. - */ -@Deprecated -@Internal -public abstract class ExecutionResultNode { - - private final ExecutionStepInfo executionStepInfo; - private final ResolvedValue resolvedValue; - private final NonNullableFieldWasNullException nonNullableFieldWasNullException; - private final ImmutableList children; - private final ImmutableList errors; - - /* - * we are trusting here the the children list is not modified on the outside (no defensive copy) - */ - protected ExecutionResultNode(ExecutionStepInfo executionStepInfo, - ResolvedValue resolvedValue, - NonNullableFieldWasNullException nonNullableFieldWasNullException, - List children, - List errors) { - this.resolvedValue = resolvedValue; - this.executionStepInfo = executionStepInfo; - this.nonNullableFieldWasNullException = nonNullableFieldWasNullException; - this.children = ImmutableList.copyOf(assertNotNull(children)); - children.forEach(Assert::assertNotNull); - this.errors = ImmutableList.copyOf(errors); - } - - public List getErrors() { - return new ArrayList<>(errors); - } - - /* - * can be null for the RootExecutionResultNode - */ - public ResolvedValue getResolvedValue() { - return resolvedValue; - } - - public MergedField getMergedField() { - return executionStepInfo.getField(); - } - - public ExecutionStepInfo getExecutionStepInfo() { - return executionStepInfo; - } - - public NonNullableFieldWasNullException getNonNullableFieldWasNullException() { - return nonNullableFieldWasNullException; - } - - public List getChildren() { - return this.children; - } - - public Optional getChildNonNullableException() { - return children.stream() - .filter(executionResultNode -> executionResultNode.getNonNullableFieldWasNullException() != null) - .map(ExecutionResultNode::getNonNullableFieldWasNullException) - .findFirst(); - } - - /** - * Creates a new ExecutionResultNode of the same specific type with the new set of result children - * - * @param children the new children for this result node - * - * @return a new ExecutionResultNode with the new result children - */ - public abstract ExecutionResultNode withNewChildren(List children); - - public abstract ExecutionResultNode withNewResolvedValue(ResolvedValue resolvedValue); - - public abstract ExecutionResultNode withNewExecutionStepInfo(ExecutionStepInfo executionStepInfo); - - - /** - * Creates a new ExecutionResultNode of the same specific type with the new error collection - * - * @param errors the new errors for this result node - * - * @return a new ExecutionResultNode with the new errors - */ - public abstract ExecutionResultNode withNewErrors(List errors); - - - @Override - public String toString() { - return "ExecutionResultNode{" + - "executionStepInfo=" + executionStepInfo + - ", resolvedValue=" + resolvedValue + - ", nonNullableFieldWasNullException=" + nonNullableFieldWasNullException + - ", children=" + children + - ", errors=" + errors + - '}'; - } -} diff --git a/src/main/java/graphql/execution/nextgen/result/LeafExecutionResultNode.java b/src/main/java/graphql/execution/nextgen/result/LeafExecutionResultNode.java deleted file mode 100644 index 01ad7b08bb..0000000000 --- a/src/main/java/graphql/execution/nextgen/result/LeafExecutionResultNode.java +++ /dev/null @@ -1,58 +0,0 @@ -package graphql.execution.nextgen.result; - -import graphql.Assert; -import graphql.GraphQLError; -import graphql.Internal; -import graphql.collect.ImmutableKit; -import graphql.execution.ExecutionStepInfo; -import graphql.execution.NonNullableFieldWasNullException; - -import java.util.ArrayList; -import java.util.Collections; -import java.util.List; - -/** - * @deprecated Jan 2022 - We have decided to deprecate the NextGen engine, and it will be removed in a future release. - */ -@Deprecated -@Internal -public class LeafExecutionResultNode extends ExecutionResultNode { - - public LeafExecutionResultNode(ExecutionStepInfo executionStepInfo, - ResolvedValue resolvedValue, - NonNullableFieldWasNullException nonNullableFieldWasNullException) { - this(executionStepInfo, resolvedValue, nonNullableFieldWasNullException, ImmutableKit.emptyList()); - } - - public LeafExecutionResultNode(ExecutionStepInfo executionStepInfo, - ResolvedValue resolvedValue, - NonNullableFieldWasNullException nonNullableFieldWasNullException, - List errors) { - super(executionStepInfo, resolvedValue, nonNullableFieldWasNullException, ImmutableKit.emptyList(), errors); - } - - - public Object getValue() { - return getResolvedValue().getCompletedValue(); - } - - @Override - public ExecutionResultNode withNewChildren(List children) { - return Assert.assertShouldNeverHappen(); - } - - @Override - public ExecutionResultNode withNewExecutionStepInfo(ExecutionStepInfo executionStepInfo) { - return new LeafExecutionResultNode(executionStepInfo, getResolvedValue(), getNonNullableFieldWasNullException(), getErrors()); - } - - @Override - public ExecutionResultNode withNewResolvedValue(ResolvedValue resolvedValue) { - return new LeafExecutionResultNode(getExecutionStepInfo(), resolvedValue, getNonNullableFieldWasNullException(), getErrors()); - } - - @Override - public ExecutionResultNode withNewErrors(List errors) { - return new LeafExecutionResultNode(getExecutionStepInfo(), getResolvedValue(), getNonNullableFieldWasNullException(), new ArrayList<>(errors)); - } -} \ No newline at end of file diff --git a/src/main/java/graphql/execution/nextgen/result/ListExecutionResultNode.java b/src/main/java/graphql/execution/nextgen/result/ListExecutionResultNode.java deleted file mode 100644 index feee93e321..0000000000 --- a/src/main/java/graphql/execution/nextgen/result/ListExecutionResultNode.java +++ /dev/null @@ -1,51 +0,0 @@ -package graphql.execution.nextgen.result; - -import graphql.GraphQLError; -import graphql.Internal; -import graphql.collect.ImmutableKit; -import graphql.execution.ExecutionStepInfo; - -import java.util.ArrayList; -import java.util.List; - -/** - * @deprecated Jan 2022 - We have decided to deprecate the NextGen engine, and it will be removed in a future release. - */ -@Deprecated -@Internal -public class ListExecutionResultNode extends ExecutionResultNode { - - public ListExecutionResultNode(ExecutionStepInfo executionStepInfo, - ResolvedValue resolvedValue, - List children) { - this(executionStepInfo, resolvedValue, children, ImmutableKit.emptyList()); - - } - - public ListExecutionResultNode(ExecutionStepInfo executionStepInfo, - ResolvedValue resolvedValue, - List children, - List errors) { - super(executionStepInfo, resolvedValue, ResultNodesUtil.newNullableException(executionStepInfo, children), children, errors); - } - - @Override - public ExecutionResultNode withNewChildren(List children) { - return new ListExecutionResultNode(getExecutionStepInfo(), getResolvedValue(), children, getErrors()); - } - - @Override - public ExecutionResultNode withNewResolvedValue(ResolvedValue resolvedValue) { - return new ListExecutionResultNode(getExecutionStepInfo(), resolvedValue, getChildren(), getErrors()); - } - - @Override - public ExecutionResultNode withNewExecutionStepInfo(ExecutionStepInfo executionStepInfo) { - return new ListExecutionResultNode(executionStepInfo, getResolvedValue(), getChildren(), getErrors()); - } - - @Override - public ExecutionResultNode withNewErrors(List errors) { - return new ListExecutionResultNode(getExecutionStepInfo(), getResolvedValue(), getChildren(), new ArrayList<>(errors)); - } -} diff --git a/src/main/java/graphql/execution/nextgen/result/NamedResultNode.java b/src/main/java/graphql/execution/nextgen/result/NamedResultNode.java deleted file mode 100644 index 36aedc24e9..0000000000 --- a/src/main/java/graphql/execution/nextgen/result/NamedResultNode.java +++ /dev/null @@ -1,30 +0,0 @@ -package graphql.execution.nextgen.result; - -import graphql.Internal; - -/** - * @deprecated Jan 2022 - We have decided to deprecate the NextGen engine, and it will be removed in a future release. - */ -@Deprecated -@Internal -public class NamedResultNode { - private final String name; - private final ExecutionResultNode node; - - public NamedResultNode(String name, ExecutionResultNode node) { - this.name = name; - this.node = node; - } - - public String getName() { - return name; - } - - public ExecutionResultNode getNode() { - return node; - } - - public NamedResultNode withNode(ExecutionResultNode newNode) { - return new NamedResultNode(name, newNode); - } -} diff --git a/src/main/java/graphql/execution/nextgen/result/ObjectExecutionResultNode.java b/src/main/java/graphql/execution/nextgen/result/ObjectExecutionResultNode.java deleted file mode 100644 index f328c05b7b..0000000000 --- a/src/main/java/graphql/execution/nextgen/result/ObjectExecutionResultNode.java +++ /dev/null @@ -1,53 +0,0 @@ -package graphql.execution.nextgen.result; - -import graphql.GraphQLError; -import graphql.Internal; -import graphql.collect.ImmutableKit; -import graphql.execution.ExecutionStepInfo; - -import java.util.ArrayList; -import java.util.List; - -/** - * @deprecated Jan 2022 - We have decided to deprecate the NextGen engine, and it will be removed in a future release. - */ -@Deprecated -@Internal -public class ObjectExecutionResultNode extends ExecutionResultNode { - - - public ObjectExecutionResultNode(ExecutionStepInfo executionStepInfo, - ResolvedValue resolvedValue, - List children) { - this(executionStepInfo, resolvedValue, children, ImmutableKit.emptyList()); - - } - - public ObjectExecutionResultNode(ExecutionStepInfo executionStepInfo, - ResolvedValue resolvedValue, - List children, - List errors) { - super(executionStepInfo, resolvedValue, ResultNodesUtil.newNullableException(executionStepInfo, children), children, errors); - } - - - @Override - public ObjectExecutionResultNode withNewChildren(List children) { - return new ObjectExecutionResultNode(getExecutionStepInfo(), getResolvedValue(), children, getErrors()); - } - - @Override - public ExecutionResultNode withNewResolvedValue(ResolvedValue resolvedValue) { - return new ObjectExecutionResultNode(getExecutionStepInfo(), resolvedValue, getChildren(), getErrors()); - } - - @Override - public ExecutionResultNode withNewExecutionStepInfo(ExecutionStepInfo executionStepInfo) { - return new ObjectExecutionResultNode(executionStepInfo, getResolvedValue(), getChildren(), getErrors()); - } - - @Override - public ExecutionResultNode withNewErrors(List errors) { - return new ObjectExecutionResultNode(getExecutionStepInfo(), getResolvedValue(), getChildren(), new ArrayList<>(errors)); - } -} diff --git a/src/main/java/graphql/execution/nextgen/result/ResolvedValue.java b/src/main/java/graphql/execution/nextgen/result/ResolvedValue.java deleted file mode 100644 index 4fc78b302d..0000000000 --- a/src/main/java/graphql/execution/nextgen/result/ResolvedValue.java +++ /dev/null @@ -1,101 +0,0 @@ -package graphql.execution.nextgen.result; - -import com.google.common.collect.ImmutableList; -import graphql.GraphQLError; -import graphql.Internal; -import graphql.collect.ImmutableKit; - -import java.util.ArrayList; -import java.util.List; -import java.util.function.Consumer; - -/** - * @deprecated Jan 2022 - We have decided to deprecate the NextGen engine, and it will be removed in a future release. - */ -@Deprecated -@Internal -public class ResolvedValue { - - private final Object completedValue; - private final Object localContext; - private final boolean nullValue; - private final ImmutableList errors; - - private ResolvedValue(Builder builder) { - this.completedValue = builder.completedValue; - this.localContext = builder.localContext; - this.nullValue = builder.nullValue; - this.errors = ImmutableList.copyOf(builder.errors); - } - - public Object getCompletedValue() { - return completedValue; - } - - public Object getLocalContext() { - return localContext; - } - - public boolean isNullValue() { - return nullValue; - } - - public List getErrors() { - return errors; - } - - public static Builder newResolvedValue() { - return new Builder(); - } - - - public ResolvedValue transform(Consumer builderConsumer) { - Builder builder = new Builder(this); - builderConsumer.accept(builder); - return builder.build(); - } - - - public static class Builder { - private Object completedValue; - private Object localContext; - private boolean nullValue; - private List errors = ImmutableKit.emptyList(); - - private Builder() { - - } - - private Builder(ResolvedValue existing) { - this.completedValue = existing.completedValue; - this.localContext = existing.localContext; - this.nullValue = existing.nullValue; - this.errors = existing.errors; - } - - public Builder completedValue(Object completedValue) { - this.completedValue = completedValue; - return this; - } - - public Builder localContext(Object localContext) { - this.localContext = localContext; - return this; - } - - public Builder nullValue(boolean nullValue) { - this.nullValue = nullValue; - return this; - } - - public Builder errors(List errors) { - this.errors = new ArrayList<>(errors); - return this; - } - - public ResolvedValue build() { - return new ResolvedValue(this); - } - } - -} diff --git a/src/main/java/graphql/execution/nextgen/result/ResultNodeAdapter.java b/src/main/java/graphql/execution/nextgen/result/ResultNodeAdapter.java deleted file mode 100644 index ccbe0910bb..0000000000 --- a/src/main/java/graphql/execution/nextgen/result/ResultNodeAdapter.java +++ /dev/null @@ -1,49 +0,0 @@ -package graphql.execution.nextgen.result; - -import graphql.Internal; -import graphql.util.NodeAdapter; -import graphql.util.NodeLocation; - -import java.util.ArrayList; -import java.util.Collections; -import java.util.List; -import java.util.Map; - -import static graphql.Assert.assertNotNull; -import static graphql.Assert.assertTrue; - -/** - * @deprecated Jan 2022 - We have decided to deprecate the NextGen engine, and it will be removed in a future release. - */ -@Deprecated -@Internal -public class ResultNodeAdapter implements NodeAdapter { - - public static final ResultNodeAdapter RESULT_NODE_ADAPTER = new ResultNodeAdapter(); - - private ResultNodeAdapter() { - - } - - @Override - public Map> getNamedChildren(ExecutionResultNode parentNode) { - return Collections.singletonMap(null, parentNode.getChildren()); - } - - @Override - public ExecutionResultNode withNewChildren(ExecutionResultNode parentNode, Map> newChildren) { - assertTrue(newChildren.size() == 1); - List childrenList = newChildren.get(null); - assertNotNull(childrenList); - return parentNode.withNewChildren(childrenList); - } - - @Override - public ExecutionResultNode removeChild(ExecutionResultNode parentNode, NodeLocation location) { - int index = location.getIndex(); - List childrenList = new ArrayList<>(parentNode.getChildren()); - assertTrue(index >= 0 && index < childrenList.size(), () -> "The remove index MUST be within the range of the children"); - childrenList.remove(index); - return parentNode.withNewChildren(childrenList); - } -} diff --git a/src/main/java/graphql/execution/nextgen/result/ResultNodeTraverser.java b/src/main/java/graphql/execution/nextgen/result/ResultNodeTraverser.java deleted file mode 100644 index c988afb32e..0000000000 --- a/src/main/java/graphql/execution/nextgen/result/ResultNodeTraverser.java +++ /dev/null @@ -1,34 +0,0 @@ -package graphql.execution.nextgen.result; - -import graphql.Internal; -import graphql.util.Traverser; -import graphql.util.TraverserVisitor; - -import java.util.Collection; - -/** - * @deprecated Jan 2022 - We have decided to deprecate the NextGen engine, and it will be removed in a future release. - */ -@Deprecated -@Internal -public class ResultNodeTraverser { - - private final Traverser traverser; - - private ResultNodeTraverser(Traverser traverser) { - this.traverser = traverser; - } - - public static ResultNodeTraverser depthFirst() { - return new ResultNodeTraverser(Traverser.depthFirst(ExecutionResultNode::getChildren, null, null)); - } - - public void traverse(TraverserVisitor visitor, ExecutionResultNode root) { - traverser.traverse(root, visitor); - } - - public void traverse(TraverserVisitor visitor, Collection roots) { - traverser.traverse(roots, visitor); - } - -} diff --git a/src/main/java/graphql/execution/nextgen/result/ResultNodesUtil.java b/src/main/java/graphql/execution/nextgen/result/ResultNodesUtil.java deleted file mode 100644 index 77005ee806..0000000000 --- a/src/main/java/graphql/execution/nextgen/result/ResultNodesUtil.java +++ /dev/null @@ -1,187 +0,0 @@ -package graphql.execution.nextgen.result; - -import graphql.Assert; -import graphql.ExecutionResult; -import graphql.ExecutionResultImpl; -import graphql.GraphQLError; -import graphql.Internal; -import graphql.execution.ExecutionStepInfo; -import graphql.execution.NonNullableFieldWasNullError; -import graphql.execution.NonNullableFieldWasNullException; -import graphql.util.NodeLocation; -import graphql.util.NodeMultiZipper; -import graphql.util.NodeZipper; -import graphql.util.TraversalControl; -import graphql.util.TraverserContext; -import graphql.util.TraverserVisitorStub; - -import java.util.ArrayList; -import java.util.Arrays; -import java.util.Collection; -import java.util.LinkedHashMap; -import java.util.List; -import java.util.Map; -import java.util.Optional; - -import static graphql.collect.ImmutableKit.map; -import static graphql.execution.nextgen.result.ResultNodeAdapter.RESULT_NODE_ADAPTER; -import static graphql.collect.ImmutableKit.emptyList; -import static java.util.Collections.singleton; - -/** - * @deprecated Jan 2022 - We have decided to deprecate the NextGen engine, and it will be removed in a future release. - */ -@Deprecated -@Internal -public class ResultNodesUtil { - - public static ExecutionResult toExecutionResult(RootExecutionResultNode root) { - ExecutionResultData executionResultData = toDataImpl(root); - return ExecutionResultImpl.newExecutionResult() - .data(executionResultData.data) - .errors(executionResultData.errors) - .build(); - } - - private static class ExecutionResultData { - Object data; - List errors; - - - public ExecutionResultData(Object data, List errors) { - this.data = data; - this.errors = errors; - } - } - - - private static ExecutionResultData data(Object data, ExecutionResultNode executionResultNode) { - List allErrors = new ArrayList<>(); - allErrors.addAll(executionResultNode.getResolvedValue().getErrors()); - allErrors.addAll(executionResultNode.getErrors()); - return new ExecutionResultData(data, allErrors); - } - - private static ExecutionResultData data(Object data, List errors) { - return new ExecutionResultData(data, errors); - } - - private static ExecutionResultData data(Object data, NonNullableFieldWasNullException exception) { - return new ExecutionResultData(data, Arrays.asList(new NonNullableFieldWasNullError(exception))); - } - - private static ExecutionResultData toDataImpl(ExecutionResultNode root) { - if (root instanceof LeafExecutionResultNode) { - return root.getResolvedValue().isNullValue() ? data(null, root) : data(((LeafExecutionResultNode) root).getValue(), root); - } - if (root instanceof ListExecutionResultNode) { - Optional childNonNullableException = root.getChildNonNullableException(); - if (childNonNullableException.isPresent()) { - return data(null, childNonNullableException.get()); - } - - List errors = new ArrayList<>(); - List data = new ArrayList<>(); - for (ExecutionResultNode child : root.getChildren()) { - ExecutionResultData erd = toDataImpl(child); - data.add(erd.data); - if (!erd.errors.isEmpty()) { - errors.addAll(erd.errors); - } - } - if (!root.getErrors().isEmpty()) { - errors.addAll(root.getErrors()); - } - return data(data, errors); - } - - if (root instanceof UnresolvedObjectResultNode) { - ExecutionStepInfo executionStepInfo = root.getExecutionStepInfo(); - return data("Not resolved : " + executionStepInfo.getPath() + " with field " + executionStepInfo.getField(), emptyList()); - } - if (root instanceof ObjectExecutionResultNode) { - Optional childrenNonNullableException = root.getChildNonNullableException(); - if (childrenNonNullableException.isPresent()) { - return data(null, childrenNonNullableException.get()); - } - Map resultMap = new LinkedHashMap<>(); - List errors = new ArrayList<>(); - root.getChildren().forEach(child -> { - ExecutionResultData executionResultData = toDataImpl(child); - resultMap.put(child.getMergedField().getResultKey(), executionResultData.data); - errors.addAll(executionResultData.errors); - }); - errors.addAll(root.getErrors()); - return data(resultMap, errors); - } - return Assert.assertShouldNeverHappen("An unexpected root type %s", root.getClass()); - } - - - public static Optional getFirstNonNullableException(Collection collection) { - return collection.stream() - .filter(executionResultNode -> executionResultNode.getNonNullableFieldWasNullException() != null) - .map(ExecutionResultNode::getNonNullableFieldWasNullException) - .findFirst(); - } - - public static NonNullableFieldWasNullException newNullableException(ExecutionStepInfo executionStepInfo, List children) { - return newNullableException(executionStepInfo, map(children, NamedResultNode::getNode)); - } - - public static Map namedNodesToMap(List namedResultNodes) { - Map result = new LinkedHashMap<>(); - for (NamedResultNode namedResultNode : namedResultNodes) { - result.put(namedResultNode.getName(), namedResultNode.getNode()); - } - return result; - } - - public static NonNullableFieldWasNullException newNullableException(ExecutionStepInfo executionStepInfo, Collection children) { - // can only happen for the root node - if (executionStepInfo == null) { - return null; - } - Assert.assertNotNull(children); - boolean listIsNonNull = executionStepInfo.isNonNullType(); - if (listIsNonNull) { - Optional firstNonNullableException = getFirstNonNullableException(children); - if (firstNonNullableException.isPresent()) { - return new NonNullableFieldWasNullException(firstNonNullableException.get()); - } - } - return null; - } - - public static List> getUnresolvedNodes(Collection roots) { - List> result = new ArrayList<>(); - - ResultNodeTraverser traverser = ResultNodeTraverser.depthFirst(); - traverser.traverse(new TraverserVisitorStub() { - @Override - public TraversalControl enter(TraverserContext context) { - if (context.thisNode() instanceof UnresolvedObjectResultNode) { - result.add(new NodeZipper<>(context.thisNode(), context.getBreadcrumbs(), RESULT_NODE_ADAPTER)); - } - return TraversalControl.CONTINUE; - } - - }, roots); - return result; - } - - public static NodeMultiZipper getUnresolvedNodes(ExecutionResultNode root) { - List> unresolvedNodes = getUnresolvedNodes(singleton(root)); - return new NodeMultiZipper<>(root, unresolvedNodes, RESULT_NODE_ADAPTER); - } - - - public static NodeLocation key(String name) { - return new NodeLocation(name, 0); - } - - public static NodeLocation index(int index) { - return new NodeLocation(null, index); - } - -} diff --git a/src/main/java/graphql/execution/nextgen/result/RootExecutionResultNode.java b/src/main/java/graphql/execution/nextgen/result/RootExecutionResultNode.java deleted file mode 100644 index b2f671c8a0..0000000000 --- a/src/main/java/graphql/execution/nextgen/result/RootExecutionResultNode.java +++ /dev/null @@ -1,58 +0,0 @@ -package graphql.execution.nextgen.result; - -import graphql.GraphQLError; -import graphql.Internal; -import graphql.collect.ImmutableKit; -import graphql.execution.ExecutionStepInfo; - -import java.util.ArrayList; -import java.util.List; - -import static graphql.Assert.assertShouldNeverHappen; - -/** - * @deprecated Jan 2022 - We have decided to deprecate the NextGen engine, and it will be removed in a future release. - */ -@Deprecated -@Internal -public class RootExecutionResultNode extends ObjectExecutionResultNode { - - - public RootExecutionResultNode(List children, List errors) { - super(null, null, children, errors); - } - - public RootExecutionResultNode(List children) { - super(null, null, children, ImmutableKit.emptyList()); - } - - @Override - public ExecutionStepInfo getExecutionStepInfo() { - return assertShouldNeverHappen("not supported at root node"); - } - - @Override - public ResolvedValue getResolvedValue() { - return assertShouldNeverHappen("not supported at root node"); - } - - @Override - public RootExecutionResultNode withNewChildren(List children) { - return new RootExecutionResultNode(children, getErrors()); - } - - @Override - public ExecutionResultNode withNewResolvedValue(ResolvedValue resolvedValue) { - return assertShouldNeverHappen("not supported at root node"); - } - - @Override - public ExecutionResultNode withNewExecutionStepInfo(ExecutionStepInfo executionStepInfo) { - return assertShouldNeverHappen("not supported at root node"); - } - - @Override - public ExecutionResultNode withNewErrors(List errors) { - return new RootExecutionResultNode(getChildren(), new ArrayList<>(errors)); - } -} diff --git a/src/main/java/graphql/execution/nextgen/result/UnresolvedObjectResultNode.java b/src/main/java/graphql/execution/nextgen/result/UnresolvedObjectResultNode.java deleted file mode 100644 index f1b5f22c7b..0000000000 --- a/src/main/java/graphql/execution/nextgen/result/UnresolvedObjectResultNode.java +++ /dev/null @@ -1,18 +0,0 @@ -package graphql.execution.nextgen.result; - -import graphql.Internal; -import graphql.collect.ImmutableKit; -import graphql.execution.ExecutionStepInfo; - -/** - * @deprecated Jan 2022 - We have decided to deprecate the NextGen engine, and it will be removed in a future release. - */ -@Deprecated -@Internal -public class UnresolvedObjectResultNode extends ObjectExecutionResultNode { - - public UnresolvedObjectResultNode(ExecutionStepInfo executionStepInfo, ResolvedValue resolvedValue) { - super(executionStepInfo, resolvedValue, ImmutableKit.emptyList(), ImmutableKit.emptyList()); - } - -} \ No newline at end of file diff --git a/src/main/java/graphql/execution/preparsed/PreparsedDocumentProvider.java b/src/main/java/graphql/execution/preparsed/PreparsedDocumentProvider.java index 3cbf13c3ac..a9d8f1e842 100644 --- a/src/main/java/graphql/execution/preparsed/PreparsedDocumentProvider.java +++ b/src/main/java/graphql/execution/preparsed/PreparsedDocumentProvider.java @@ -1,6 +1,7 @@ package graphql.execution.preparsed; +import graphql.DeprecatedAt; import graphql.ExecutionInput; import graphql.PublicSpi; @@ -27,6 +28,7 @@ public interface PreparsedDocumentProvider { * @deprecated - use {@link #getDocumentAsync(ExecutionInput executionInput, Function parseAndValidateFunction)} */ @Deprecated + @DeprecatedAt("2021-12-06") PreparsedDocumentEntry getDocument(ExecutionInput executionInput, Function parseAndValidateFunction); /** diff --git a/src/main/java/graphql/execution/preparsed/persisted/PersistedQueryCache.java b/src/main/java/graphql/execution/preparsed/persisted/PersistedQueryCache.java index 3894b37f0e..034e4b4ebb 100644 --- a/src/main/java/graphql/execution/preparsed/persisted/PersistedQueryCache.java +++ b/src/main/java/graphql/execution/preparsed/persisted/PersistedQueryCache.java @@ -1,5 +1,6 @@ package graphql.execution.preparsed.persisted; +import graphql.DeprecatedAt; import graphql.ExecutionInput; import graphql.PublicSpi; import graphql.execution.preparsed.PreparsedDocumentEntry; @@ -18,20 +19,21 @@ public interface PersistedQueryCache { * If its present in cache then it must return a PreparsedDocumentEntry where {@link graphql.execution.preparsed.PreparsedDocumentEntry#getDocument()} * is already parsed and validated. This will be passed onto the graphql engine as is. *

- * If its a valid query id but its no present in cache, (cache miss) then you need to call back the "onCacheMiss" function with associated query text. - * This will be compiled and validated by the graphql engine and the a PreparsedDocumentEntry will be passed back ready for you to cache it. + * If it's a valid query id but its no present in cache, (cache miss) then you need to call back the "onCacheMiss" function with associated query text. + * This will be compiled and validated by the graphql engine and the PreparsedDocumentEntry will be passed back ready for you to cache it. *

- * If its not a valid query id then throw a {@link graphql.execution.preparsed.persisted.PersistedQueryNotFound} to indicate this. + * If it's not a valid query id then throw a {@link graphql.execution.preparsed.persisted.PersistedQueryNotFound} to indicate this. * * @param persistedQueryId the persisted query id * @param executionInput the original execution input - * @param onCacheMiss the call back should it be a valid query id but its not currently not in the cache + * @param onCacheMiss the call back should it be a valid query id but it's not currently in the cache * @return a parsed and validated PreparsedDocumentEntry where {@link graphql.execution.preparsed.PreparsedDocumentEntry#getDocument()} is set * @throws graphql.execution.preparsed.persisted.PersistedQueryNotFound if the query id is not know at all and you have no query text * * @deprecated - use {@link #getPersistedQueryDocumentAsync(Object persistedQueryId, ExecutionInput executionInput, PersistedQueryCacheMiss onCacheMiss)} */ @Deprecated + @DeprecatedAt("2021-12-06") PreparsedDocumentEntry getPersistedQueryDocument(Object persistedQueryId, ExecutionInput executionInput, PersistedQueryCacheMiss onCacheMiss) throws PersistedQueryNotFound; /** @@ -40,14 +42,14 @@ public interface PersistedQueryCache { * If its present in cache then it must return a PreparsedDocumentEntry where {@link graphql.execution.preparsed.PreparsedDocumentEntry#getDocument()} * is already parsed and validated. This will be passed onto the graphql engine as is. *

- * If its a valid query id but its no present in cache, (cache miss) then you need to call back the "onCacheMiss" function with associated query text. - * This will be compiled and validated by the graphql engine and the a PreparsedDocumentEntry will be passed back ready for you to cache it. + * If it's a valid query id but its no present in cache, (cache miss) then you need to call back the "onCacheMiss" function with associated query text. + * This will be compiled and validated by the graphql engine and the PreparsedDocumentEntry will be passed back ready for you to cache it. *

- * If its not a valid query id then throw a {@link graphql.execution.preparsed.persisted.PersistedQueryNotFound} to indicate this. + * If it's not a valid query id then throw a {@link graphql.execution.preparsed.persisted.PersistedQueryNotFound} to indicate this. * * @param persistedQueryId the persisted query id * @param executionInput the original execution input - * @param onCacheMiss the call back should it be a valid query id but its not currently not in the cache + * @param onCacheMiss the call back should it be a valid query id but it's not currently in the cache * @return a promise to parsed and validated {@link PreparsedDocumentEntry} where {@link graphql.execution.preparsed.PreparsedDocumentEntry#getDocument()} is set * @throws graphql.execution.preparsed.persisted.PersistedQueryNotFound if the query id is not know at all and you have no query text */ diff --git a/src/main/java/graphql/i18n/I18n.java b/src/main/java/graphql/i18n/I18n.java index dd9efae54e..6898f7be8b 100644 --- a/src/main/java/graphql/i18n/I18n.java +++ b/src/main/java/graphql/i18n/I18n.java @@ -4,6 +4,7 @@ import graphql.VisibleForTesting; import java.text.MessageFormat; +import java.util.List; import java.util.Locale; import java.util.MissingResourceException; import java.util.ResourceBundle; @@ -14,10 +15,13 @@ @Internal public class I18n { + /** * This enum is a type safe way to control what resource bundle to load from */ public enum BundleType { + Parsing, + Scalars, Validation, Execution, General; @@ -30,12 +34,19 @@ public enum BundleType { } private final ResourceBundle resourceBundle; + private final Locale locale; @VisibleForTesting protected I18n(BundleType bundleType, Locale locale) { assertNotNull(bundleType); assertNotNull(locale); - this.resourceBundle = ResourceBundle.getBundle(bundleType.baseName, locale); + this.locale = locale; + // load the resource bundle with this classes class loader - to help avoid confusion in complicated worlds + // like OSGI + this.resourceBundle = ResourceBundle.getBundle(bundleType.baseName, locale, I18n.class.getClassLoader()); } + + public Locale getLocale() { + return locale; } public ResourceBundle getResourceBundle() { @@ -55,16 +66,29 @@ public static I18n i18n(BundleType bundleType, Locale locale) { * * @return the formatted I18N message */ - @SuppressWarnings("UnnecessaryLocalVariable") public String msg(String msgKey, Object... msgArgs) { + return msgImpl(msgKey, msgArgs); + } + + /** + * Creates an I18N message using the key and arguments + * + * @param msgKey the key in the underlying message bundle + * @param msgArgs the message arguments + * + * @return the formatted I18N message + */ + public String msg(String msgKey, List msgArgs) { + return msgImpl(msgKey, msgArgs.toArray()); + } + + private String msgImpl(String msgKey, Object[] msgArgs) { String msgPattern = null; try { msgPattern = resourceBundle.getString(msgKey); } catch (MissingResourceException e) { assertShouldNeverHappen("There must be a resource bundle key called %s", msgKey); } - - String formattedMsg = new MessageFormat(msgPattern).format(msgArgs); - return formattedMsg; + return new MessageFormat(msgPattern).format(msgArgs); } } diff --git a/src/main/java/graphql/introspection/Introspection.java b/src/main/java/graphql/introspection/Introspection.java index 3b8b5f40f0..9b874f7f1b 100644 --- a/src/main/java/graphql/introspection/Introspection.java +++ b/src/main/java/graphql/introspection/Introspection.java @@ -3,6 +3,7 @@ import com.google.common.collect.ImmutableSet; import graphql.Assert; +import graphql.GraphQLContext; import graphql.Internal; import graphql.PublicApi; import graphql.execution.ValuesResolver; @@ -36,8 +37,10 @@ import java.util.HashSet; import java.util.LinkedHashMap; import java.util.List; +import java.util.Locale; import java.util.Map; import java.util.Set; +import java.util.function.Function; import java.util.stream.Collectors; import static graphql.Assert.assertTrue; @@ -54,7 +57,6 @@ import static graphql.schema.GraphQLTypeUtil.simplePrint; import static graphql.schema.GraphQLTypeUtil.unwrapAllAs; import static graphql.schema.GraphQLTypeUtil.unwrapOne; -import static graphql.schema.visibility.DefaultGraphqlFieldVisibility.DEFAULT_FIELD_VISIBILITY; @PublicApi public class Introspection { @@ -64,6 +66,27 @@ private static void register(GraphQLFieldsContainer parentType, String fieldName introspectionDataFetchers.put(coordinates(parentType.getName(), fieldName), introspectionDataFetcher); } + /** + * To help runtimes such as graalvm, we make sure we have an explicit data fetchers rather then use {@link graphql.schema.PropertyDataFetcher} + * and its reflective mechanisms. This is not reflective because we have the class + * + * @param parentType the containing parent type + * @param fieldName the field name + * @param targetClass the target class of the getter + * @param getter the function to call to get a value of T + * @param for two + */ + private static void register(GraphQLFieldsContainer parentType, String fieldName, Class targetClass, Function getter) { + IntrospectionDataFetcher dataFetcher = env -> { + Object source = env.getSource(); + if (targetClass.isInstance(source)) { + return getter.apply(targetClass.cast(source)); + } + return null; + }; + introspectionDataFetchers.put(coordinates(parentType.getName(), fieldName), dataFetcher); + } + @Internal public static void addCodeForIntrospectionTypes(GraphQLCodeRegistry.Builder codeRegistry) { // place the system __ fields into the mix. They have no parent types @@ -89,7 +112,7 @@ public enum TypeKind { public static final GraphQLEnumType __TypeKind = GraphQLEnumType.newEnum() .name("__TypeKind") .description("An enum describing what kind of type a given __Type is") - .value("SCALAR", TypeKind.SCALAR, "Indicates this type is a scalar. 'specifiedByUrl' is a valid field") + .value("SCALAR", TypeKind.SCALAR, "Indicates this type is a scalar. 'specifiedByURL' is a valid field") .value("OBJECT", TypeKind.OBJECT, "Indicates this type is an object. `fields` and `interfaces` are valid fields.") .value("INTERFACE", TypeKind.INTERFACE, "Indicates this type is an interface. `fields` and `possibleTypes` are valid fields.") .value("UNION", TypeKind.UNION, "Indicates this type is a union. `possibleTypes` is a valid field.") @@ -159,18 +182,22 @@ public enum TypeKind { .build(); static { - register(__InputValue, "defaultValue", environment -> { + IntrospectionDataFetcher defaultValueDataFetcher = environment -> { Object type = environment.getSource(); if (type instanceof GraphQLArgument) { GraphQLArgument inputField = (GraphQLArgument) type; - return inputField.hasSetDefaultValue() ? printDefaultValue(inputField.getArgumentDefaultValue(), inputField.getType()) : null; + return inputField.hasSetDefaultValue() + ? printDefaultValue(inputField.getArgumentDefaultValue(), inputField.getType(), environment.getGraphQlContext(), environment.getLocale()) + : null; } else if (type instanceof GraphQLInputObjectField) { GraphQLInputObjectField inputField = (GraphQLInputObjectField) type; - return inputField.hasSetDefaultValue() ? printDefaultValue(inputField.getInputFieldDefaultValue(), inputField.getType()) : null; + return inputField.hasSetDefaultValue() + ? printDefaultValue(inputField.getInputFieldDefaultValue(), inputField.getType(), environment.getGraphQlContext(), environment.getLocale()) + : null; } return null; - }); - register(__InputValue, "isDeprecated", environment -> { + }; + IntrospectionDataFetcher isDeprecatedDataFetcher = environment -> { Object type = environment.getSource(); if (type instanceof GraphQLArgument) { return ((GraphQLArgument) type).isDeprecated(); @@ -178,13 +205,36 @@ public enum TypeKind { return ((GraphQLInputObjectField) type).isDeprecated(); } return null; - }); + }; + IntrospectionDataFetcher typeDataFetcher = environment -> { + Object type = environment.getSource(); + if (type instanceof GraphQLArgument) { + return ((GraphQLArgument) type).getType(); + } else if (type instanceof GraphQLInputObjectField) { + return ((GraphQLInputObjectField) type).getType(); + } + return null; + }; + IntrospectionDataFetcher deprecationReasonDataFetcher = environment -> { + Object type = environment.getSource(); + if (type instanceof GraphQLArgument) { + return ((GraphQLArgument) type).getDeprecationReason(); + } else if (type instanceof GraphQLInputObjectField) { + return ((GraphQLInputObjectField) type).getDeprecationReason(); + } + return null; + }; + register(__InputValue, "name", nameDataFetcher); register(__InputValue, "description", descriptionDataFetcher); + register(__InputValue, "type", typeDataFetcher); + register(__InputValue, "defaultValue", defaultValueDataFetcher); + register(__InputValue, "isDeprecated", isDeprecatedDataFetcher); + register(__InputValue, "deprecationReason", deprecationReasonDataFetcher); } - private static String printDefaultValue(InputValueWithState inputValueWithState, GraphQLInputType type) { - return AstPrinter.printAst(ValuesResolver.valueToLiteral(DEFAULT_FIELD_VISIBILITY, inputValueWithState, type)); + private static String printDefaultValue(InputValueWithState inputValueWithState, GraphQLInputType type, GraphQLContext graphQLContext, Locale locale) { + return AstPrinter.printAst(ValuesResolver.valueToLiteral(inputValueWithState, type, graphQLContext, locale)); } @@ -215,20 +265,20 @@ private static String printDefaultValue(InputValueWithState inputValueWithState, .build(); static { - register(__Field, "args", environment -> { + IntrospectionDataFetcher argsDataFetcher = environment -> { Object type = environment.getSource(); GraphQLFieldDefinition fieldDef = (GraphQLFieldDefinition) type; Boolean includeDeprecated = environment.getArgument("includeDeprecated"); return fieldDef.getArguments().stream() .filter(arg -> includeDeprecated || !arg.isDeprecated()) .collect(Collectors.toList()); - }); - register(__Field, "isDeprecated", environment -> { - Object type = environment.getSource(); - return ((GraphQLFieldDefinition) type).isDeprecated(); - }); + }; register(__Field, "name", nameDataFetcher); register(__Field, "description", descriptionDataFetcher); + register(__Field, "args", argsDataFetcher); + register(__Field, "type", GraphQLFieldDefinition.class, GraphQLFieldDefinition::getType); + register(__Field, "isDeprecated", GraphQLFieldDefinition.class, GraphQLFieldDefinition::isDeprecated); + register(__Field, "deprecationReason", GraphQLFieldDefinition.class, GraphQLFieldDefinition::getDeprecationReason); } @@ -249,12 +299,10 @@ private static String printDefaultValue(InputValueWithState inputValueWithState, .build(); static { - register(__EnumValue, "isDeprecated", environment -> { - GraphQLEnumValueDefinition enumValue = environment.getSource(); - return enumValue.isDeprecated(); - }); register(__EnumValue, "name", nameDataFetcher); register(__EnumValue, "description", descriptionDataFetcher); + register(__EnumValue, "isDeprecated", GraphQLEnumValueDefinition.class, GraphQLEnumValueDefinition::isDeprecated); + register(__EnumValue, "deprecationReason", GraphQLEnumValueDefinition.class, GraphQLEnumValueDefinition::getDeprecationReason); } @@ -393,21 +441,27 @@ private static String printDefaultValue(InputValueWithState inputValueWithState, .name("ofType") .type(typeRef("__Type"))) .field(newFieldDefinition() - .name("specifiedByUrl") + .name("specifiedByURL") .type(GraphQLString)) + .field(newFieldDefinition() + .name("specifiedByUrl") + .type(GraphQLString) + .deprecate("This legacy name has been replaced by `specifiedByURL`") + ) .build(); static { register(__Type, "kind", kindDataFetcher); + register(__Type, "name", nameDataFetcher); + register(__Type, "description", descriptionDataFetcher); register(__Type, "fields", fieldsFetcher); register(__Type, "interfaces", interfacesFetcher); register(__Type, "possibleTypes", possibleTypesFetcher); register(__Type, "enumValues", enumValuesTypesFetcher); register(__Type, "inputFields", inputFieldsFetcher); register(__Type, "ofType", OfTypeFetcher); - register(__Type, "name", nameDataFetcher); - register(__Type, "description", descriptionDataFetcher); - register(__Type, "specifiedByUrl", specifiedByUrlDataFetcher); + register(__Type, "specifiedByURL", specifiedByUrlDataFetcher); + register(__Type, "specifiedByUrl", specifiedByUrlDataFetcher); // note that this field is deprecated } @@ -486,38 +540,25 @@ public enum DirectiveLocation { .name("includeDeprecated") .type(GraphQLBoolean) .defaultValueProgrammatic(false))) - .field(newFieldDefinition() - .name("onOperation") - .type(GraphQLBoolean) - .deprecate("Use `locations`.")) - .field(newFieldDefinition() - .name("onFragment") - .type(GraphQLBoolean) - .deprecate("Use `locations`.")) - .field(newFieldDefinition() - .name("onField") - .type(GraphQLBoolean) - .deprecate("Use `locations`.")) .build(); static { - register(__Directive, "locations", environment -> { + IntrospectionDataFetcher locationsDataFetcher = environment -> { GraphQLDirective directive = environment.getSource(); return new ArrayList<>(directive.validLocations()); - }); - register(__Directive, "args", environment -> { + }; + IntrospectionDataFetcher argsDataFetcher = environment -> { GraphQLDirective directive = environment.getSource(); Boolean includeDeprecated = environment.getArgument("includeDeprecated"); return directive.getArguments().stream() .filter(arg -> includeDeprecated || !arg.isDeprecated()) .collect(Collectors.toList()); - }); + }; register(__Directive, "name", nameDataFetcher); register(__Directive, "description", descriptionDataFetcher); - register(__Directive, "isRepeatable", environment -> { - GraphQLDirective directive = environment.getSource(); - return directive.isRepeatable(); - }); + register(__Directive, "isRepeatable", GraphQLDirective.class, GraphQLDirective::isRepeatable); + register(__Directive, "locations", locationsDataFetcher); + register(__Directive, "args", argsDataFetcher); } public static final GraphQLObjectType __Schema = newObject() @@ -551,24 +592,14 @@ public enum DirectiveLocation { .build(); static { - register(__Schema, "description", environment -> environment.getGraphQLSchema().getDescription()); - register(__Schema, "types", environment -> { - GraphQLSchema schema = environment.getSource(); - return schema.getAllTypesAsList(); - }); - register(__Schema, "queryType", environment -> { - GraphQLSchema schema = environment.getSource(); - return schema.getQueryType(); - }); - register(__Schema, "mutationType", environment -> { - GraphQLSchema schema = environment.getSource(); - return schema.getMutationType(); - }); - register(__Schema, "directives", environment -> environment.getGraphQLSchema().getDirectives()); - register(__Schema, "subscriptionType", environment -> { - GraphQLSchema schema = environment.getSource(); - return schema.getSubscriptionType(); - }); + IntrospectionDataFetcher descriptionsDataFetcher = environment -> environment.getGraphQLSchema().getDescription(); + + register(__Schema, "description", descriptionsDataFetcher); + register(__Schema, "types", GraphQLSchema.class, GraphQLSchema::getAllTypesAsList); + register(__Schema, "queryType", GraphQLSchema.class, GraphQLSchema::getQueryType); + register(__Schema, "mutationType", GraphQLSchema.class, GraphQLSchema::getMutationType); + register(__Schema, "directives", GraphQLSchema.class, GraphQLSchema::getDirectives); + register(__Schema, "subscriptionType", GraphQLSchema.class, GraphQLSchema::getSubscriptionType); } public static final GraphQLFieldDefinition SchemaMetaFieldDef = buildSchemaField(__Schema); diff --git a/src/main/java/graphql/introspection/IntrospectionQuery.java b/src/main/java/graphql/introspection/IntrospectionQuery.java index f93617951f..694ccaab42 100644 --- a/src/main/java/graphql/introspection/IntrospectionQuery.java +++ b/src/main/java/graphql/introspection/IntrospectionQuery.java @@ -4,106 +4,10 @@ @PublicApi public interface IntrospectionQuery { - - String INTROSPECTION_QUERY = "\n" + - " query IntrospectionQuery {\n" + - " __schema {\n" + - " queryType { name }\n" + - " mutationType { name }\n" + - " subscriptionType { name }\n" + - " types {\n" + - " ...FullType\n" + - " }\n" + - " directives {\n" + - " name\n" + - " description\n" + - " locations\n" + - " args(includeDeprecated: true) {\n" + - " ...InputValue\n" + - " }\n" + - " isRepeatable\n" + - " }\n" + - " }\n" + - " }\n" + - "\n" + - " fragment FullType on __Type {\n" + - " kind\n" + - " name\n" + - " description\n" + - " fields(includeDeprecated: true) {\n" + - " name\n" + - " description\n" + - " args(includeDeprecated: true) {\n" + - " ...InputValue\n" + - " }\n" + - " type {\n" + - " ...TypeRef\n" + - " }\n" + - " isDeprecated\n" + - " deprecationReason\n" + - " }\n" + - " inputFields(includeDeprecated: true) {\n" + - " ...InputValue\n" + - " }\n" + - " interfaces {\n" + - " ...TypeRef\n" + - " }\n" + - " enumValues(includeDeprecated: true) {\n" + - " name\n" + - " description\n" + - " isDeprecated\n" + - " deprecationReason\n" + - " }\n" + - " possibleTypes {\n" + - " ...TypeRef\n" + - " }\n" + - " }\n" + - "\n" + - " fragment InputValue on __InputValue {\n" + - " name\n" + - " description\n" + - " type { ...TypeRef }\n" + - " defaultValue\n" + - " isDeprecated\n" + - " deprecationReason\n" + - " }\n" + - "\n" + - // - // The depth of the types is actually an arbitrary decision. It could be any depth in fact. This depth - // was taken from GraphIQL https://github.com/graphql/graphiql/blob/master/src/utility/introspectionQueries.js - // which uses 7 levels and hence could represent a type like say [[[[[Float!]]]]] - // - "fragment TypeRef on __Type {\n" + - " kind\n" + - " name\n" + - " ofType {\n" + - " kind\n" + - " name\n" + - " ofType {\n" + - " kind\n" + - " name\n" + - " ofType {\n" + - " kind\n" + - " name\n" + - " ofType {\n" + - " kind\n" + - " name\n" + - " ofType {\n" + - " kind\n" + - " name\n" + - " ofType {\n" + - " kind\n" + - " name\n" + - " ofType {\n" + - " kind\n" + - " name\n" + - " }\n" + - " }\n" + - " }\n" + - " }\n" + - " }\n" + - " }\n" + - " }\n" + - " }\n" + - "\n"; + /** + * This is the default introspection query provided by graphql-java + * + * @see IntrospectionQueryBuilder for ways to customize the introspection query + */ + String INTROSPECTION_QUERY = IntrospectionQueryBuilder.build(); } diff --git a/src/main/java/graphql/introspection/IntrospectionQueryBuilder.java b/src/main/java/graphql/introspection/IntrospectionQueryBuilder.java new file mode 100644 index 0000000000..b49af872e4 --- /dev/null +++ b/src/main/java/graphql/introspection/IntrospectionQueryBuilder.java @@ -0,0 +1,389 @@ +package graphql.introspection; + +import com.google.common.collect.ImmutableList; +import graphql.PublicApi; +import graphql.language.AstPrinter; +import graphql.language.BooleanValue; +import graphql.language.Document; +import graphql.language.OperationDefinition; +import graphql.language.SelectionSet; + +import java.util.Arrays; +import java.util.List; +import java.util.Objects; + +import static graphql.language.Argument.newArgument; +import static graphql.language.Document.newDocument; +import static graphql.language.Field.newField; +import static graphql.language.FragmentDefinition.newFragmentDefinition; +import static graphql.language.FragmentSpread.newFragmentSpread; +import static graphql.language.OperationDefinition.newOperationDefinition; +import static graphql.language.SelectionSet.newSelectionSet; +import static graphql.language.TypeName.newTypeName; +import static java.util.stream.Collectors.toList; + +/** + * {@link IntrospectionQueryBuilder} allows you to build introspection queries controlled + * by the options you specify + */ +@PublicApi +public class IntrospectionQueryBuilder { + public static class Options { + + private final boolean descriptions; + + private final boolean specifiedByUrl; + + private final boolean directiveIsRepeatable; + + private final boolean schemaDescription; + + private final boolean inputValueDeprecation; + + private final int typeRefFragmentDepth; + + private Options(boolean descriptions, + boolean specifiedByUrl, + boolean directiveIsRepeatable, + boolean schemaDescription, + boolean inputValueDeprecation, + int typeRefFragmentDepth) { + this.descriptions = descriptions; + this.specifiedByUrl = specifiedByUrl; + this.directiveIsRepeatable = directiveIsRepeatable; + this.schemaDescription = schemaDescription; + this.inputValueDeprecation = inputValueDeprecation; + this.typeRefFragmentDepth = typeRefFragmentDepth; + } + + public boolean isDescriptions() { + return descriptions; + } + + public boolean isSpecifiedByUrl() { + return specifiedByUrl; + } + + public boolean isDirectiveIsRepeatable() { + return directiveIsRepeatable; + } + + public boolean isSchemaDescription() { + return schemaDescription; + } + + public boolean isInputValueDeprecation() { + return inputValueDeprecation; + } + + public int getTypeRefFragmentDepth() { + return typeRefFragmentDepth; + } + + public static Options defaultOptions() { + return new Options( + true, + false, + true, + false, + true, + 7 + ); + } + + /** + * This will allow you to include description fields in the introspection query + * + * @param flag whether to include them + * + * @return options + */ + public Options descriptions(boolean flag) { + return new Options(flag, + this.specifiedByUrl, + this.directiveIsRepeatable, + this.schemaDescription, + this.inputValueDeprecation, + this.typeRefFragmentDepth); + } + + /** + * This will allow you to include the `specifiedByURL` field for scalar types in the introspection query. + * + * @param flag whether to include them + * + * @return options + */ + public Options specifiedByUrl(boolean flag) { + return new Options(this.descriptions, + flag, + this.directiveIsRepeatable, + this.schemaDescription, + this.inputValueDeprecation, + this.typeRefFragmentDepth); + } + + /** + * This will allow you to include the `isRepeatable` field for directives in the introspection query. + * + * @param flag whether to include them + * + * @return options + */ + public Options directiveIsRepeatable(boolean flag) { + return new Options(this.descriptions, + this.specifiedByUrl, + flag, + this.schemaDescription, + this.inputValueDeprecation, + this.typeRefFragmentDepth); + } + + /** + * This will allow you to include the `description` field for the schema type in the introspection query. + * + * @param flag whether to include them + * + * @return options + */ + public Options schemaDescription(boolean flag) { + return new Options(this.descriptions, + this.specifiedByUrl, + this.directiveIsRepeatable, + flag, + this.inputValueDeprecation, + this.typeRefFragmentDepth); + } + + /** + * This will allow you to include deprecated input fields in the introspection query. + * + * @param flag whether to include them + * + * @return options + */ + public Options inputValueDeprecation(boolean flag) { + return new Options(this.descriptions, + this.specifiedByUrl, + this.directiveIsRepeatable, + this.schemaDescription, + flag, + this.typeRefFragmentDepth); + } + + /** + * This will allow you to control the depth of the `TypeRef` fragment in the introspection query. + * + * @param typeRefFragmentDepth the depth of the `TypeRef` fragment. + * + * @return options + */ + public Options typeRefFragmentDepth(int typeRefFragmentDepth) { + return new Options(this.descriptions, + this.specifiedByUrl, + this.directiveIsRepeatable, + this.schemaDescription, + this.inputValueDeprecation, + typeRefFragmentDepth); + } + } + + @SafeVarargs + private static List filter(T... args) { + return Arrays.stream(args).filter(Objects::nonNull).collect(toList()); + } + + /** + * This will build an introspection query in {@link Document} form + * + * @param options the options to use + * + * @return an introspection query in document form + */ + public static Document buildDocument(Options options) { + SelectionSet schemaSelectionSet = newSelectionSet().selections(filter( + options.schemaDescription ? newField("description").build() : null, + newField("queryType", newSelectionSet() + .selection(newField("name").build()) + .build() + ) + .build(), + newField("mutationType", newSelectionSet() + .selection(newField("name").build()) + .build() + ) + .build(), + newField("subscriptionType", newSelectionSet() + .selection(newField("name").build()) + .build() + ) + .build(), + newField("types", newSelectionSet() + .selection(newFragmentSpread("FullType").build()) + .build() + ) + .build(), + newField("directives", newSelectionSet().selections(filter( + newField("name").build(), + options.descriptions ? newField("description").build() : null, + newField("locations").build(), + newField("args") + .arguments(filter( + options.inputValueDeprecation ? newArgument("includeDeprecated", BooleanValue.of(true)).build() : null + )) + .selectionSet(newSelectionSet() + .selection(newFragmentSpread("InputValue").build()) + .build() + ) + .build(), + options.directiveIsRepeatable ? newField("isRepeatable").build() : null + )) + .build() + ) + .build() + ) + ).build(); + + SelectionSet fullTypeSelectionSet = newSelectionSet().selections(filter( + newField("kind").build(), + newField("name").build(), + options.descriptions ? newField("description").build() : null, + options.specifiedByUrl ? newField("specifiedByURL").build() : null, + newField("fields") + .arguments(ImmutableList.of( + newArgument("includeDeprecated", BooleanValue.of(true)).build() + )) + .selectionSet(newSelectionSet().selections(filter( + newField("name").build(), + options.descriptions ? newField("description").build() : null, + newField("args") + .arguments(filter( + options.inputValueDeprecation ? newArgument("includeDeprecated", BooleanValue.of(true)).build() : null + )) + .selectionSet(newSelectionSet() + .selection(newFragmentSpread("InputValue").build()) + .build() + ) + .build(), + newField("type", newSelectionSet() + .selection(newFragmentSpread("TypeRef").build()) + .build() + ) + .build(), + newField("isDeprecated").build(), + newField("deprecationReason").build() + )).build() + ) + .build(), + newField("inputFields") + .arguments(filter( + options.inputValueDeprecation ? newArgument("includeDeprecated", BooleanValue.of(true)).build() : null + )) + .selectionSet(newSelectionSet() + .selection(newFragmentSpread("InputValue").build()) + .build() + ) + .build(), + newField("interfaces", newSelectionSet() + .selection(newFragmentSpread("TypeRef").build()) + .build() + ) + .build(), + newField("enumValues") + .arguments(ImmutableList.of( + newArgument("includeDeprecated", BooleanValue.of(true)).build() + )) + .selectionSet(newSelectionSet().selections(filter( + newField("name").build(), + options.descriptions ? newField("description").build() : null, + newField("isDeprecated").build(), + newField("deprecationReason").build() + )) + .build() + ) + .build(), + newField("possibleTypes", newSelectionSet() + .selection(newFragmentSpread("TypeRef").build()) + .build() + ) + .build() + )).build(); + + SelectionSet inputValueSelectionSet = newSelectionSet().selections(filter( + newField("name").build(), + options.descriptions ? newField("description").build() : null, + newField("type", newSelectionSet() + .selection(newFragmentSpread("TypeRef").build()) + .build() + ) + .build(), + newField("defaultValue").build(), + options.inputValueDeprecation ? newField("isDeprecated").build() : null, + options.inputValueDeprecation ? newField("deprecationReason").build() : null + )).build(); + + SelectionSet typeRefSelectionSet = newSelectionSet().selections(filter( + newField("kind").build(), + newField("name").build() + )).build(); + + for (int i = options.typeRefFragmentDepth; i > 0; i -= 1) { + typeRefSelectionSet = newSelectionSet().selections(filter( + newField("kind").build(), + newField("name").build(), + newField("ofType", typeRefSelectionSet).build() + )).build(); + } + + return newDocument() + .definition(newOperationDefinition() + .operation(OperationDefinition.Operation.QUERY) + .name("IntrospectionQuery") + .selectionSet(newSelectionSet() + .selection(newField("__schema", schemaSelectionSet).build()) + .build() + ) + .build() + ) + .definition(newFragmentDefinition() + .name("FullType") + .typeCondition(newTypeName().name("__Type").build()) + .selectionSet(fullTypeSelectionSet) + .build() + ) + .definition(newFragmentDefinition() + .name("InputValue") + .typeCondition(newTypeName().name("__InputValue").build()) + .selectionSet(inputValueSelectionSet) + .build() + ) + .definition(newFragmentDefinition() + .name("TypeRef") + .typeCondition(newTypeName().name("__Type").build()) + .selectionSet(typeRefSelectionSet) + .build() + ) + .build(); + } + + /** + * This will build an introspection query in {@link String} form based on the options you provide + * + * @param options the options to use + * + * @return an introspection query in string form + */ + + public static String build(Options options) { + return AstPrinter.printAst(buildDocument(options)); + } + + /** + * This will build a default introspection query in {@link String} form + * + * @return an introspection query in string form + */ + public static String build() { + return build(Options.defaultOptions()); + } +} \ No newline at end of file diff --git a/src/main/java/graphql/introspection/IntrospectionWithDirectivesSupport.java b/src/main/java/graphql/introspection/IntrospectionWithDirectivesSupport.java index 9669a0e66d..a533dd1e2d 100644 --- a/src/main/java/graphql/introspection/IntrospectionWithDirectivesSupport.java +++ b/src/main/java/graphql/introspection/IntrospectionWithDirectivesSupport.java @@ -13,6 +13,7 @@ import graphql.schema.GraphQLCodeRegistry; import graphql.schema.GraphQLDirective; import graphql.schema.GraphQLDirectiveContainer; +import graphql.schema.GraphQLNamedSchemaElement; import graphql.schema.GraphQLObjectType; import graphql.schema.GraphQLSchema; import graphql.schema.GraphQLSchemaElement; @@ -204,11 +205,21 @@ private GraphQLObjectType addAppliedDirectives(GraphQLObjectType originalType, G DataFetcher argValueDF = env -> { final GraphQLAppliedDirectiveArgument argument = env.getSource(); InputValueWithState value = argument.getArgumentValue(); - Node literal = ValuesResolver.valueToLiteral(value, argument.getType()); + Node literal = ValuesResolver.valueToLiteral(value, argument.getType(), env.getGraphQlContext(), env.getLocale()); return AstPrinter.printAst(literal); }; + DataFetcher nameDF = env -> { + if (env.getSource() instanceof GraphQLNamedSchemaElement) { + return ((GraphQLNamedSchemaElement) env.getSource()).getName(); + } + return null; + }; + codeRegistry.dataFetcher(coordinates(objectType, "appliedDirectives"), df); + codeRegistry.dataFetcher(coordinates(appliedDirectiveType, "name"), nameDF); codeRegistry.dataFetcher(coordinates(appliedDirectiveType, "args"), argsDF); + + codeRegistry.dataFetcher(coordinates(directiveArgumentType, "name"), nameDF); codeRegistry.dataFetcher(coordinates(directiveArgumentType, "value"), argValueDF); return objectType; } diff --git a/src/main/java/graphql/language/AstPrinter.java b/src/main/java/graphql/language/AstPrinter.java index a6322cdda4..218dabd59e 100644 --- a/src/main/java/graphql/language/AstPrinter.java +++ b/src/main/java/graphql/language/AstPrinter.java @@ -25,7 +25,7 @@ public class AstPrinter { private final boolean compactMode; - private AstPrinter(boolean compactMode) { + AstPrinter(boolean compactMode) { this.compactMode = compactMode; printers.put(Argument.class, argument()); printers.put(ArrayValue.class, value()); @@ -463,11 +463,11 @@ private String node(Node node, Class startClass) { } @SuppressWarnings("unchecked") - private NodePrinter _findPrinter(Node node) { + NodePrinter _findPrinter(Node node) { return _findPrinter(node, null); } - private NodePrinter _findPrinter(Node node, Class startClass) { + NodePrinter _findPrinter(Node node, Class startClass) { if (node == null) { return (out, type) -> { }; @@ -689,7 +689,7 @@ public static void printAst(Writer writer, Node node) { /** * This will print the Ast node in graphql language format in a compact manner, with no new lines - * and comments stripped out of the text. + * and descriptions stripped out of the text. * * @param node the AST node to print * @@ -712,7 +712,16 @@ private static void printImpl(StringBuilder writer, Node node, boolean compactMo * * @param the type of node */ - private interface NodePrinter { + interface NodePrinter { void print(StringBuilder out, T node); } + + /** + * Allow subclasses to replace a printer for a specific {@link Node} + * @param nodeClass the class of the {@link Node} + * @param nodePrinter the custom {@link NodePrinter} + */ + void replacePrinter(Class nodeClass, NodePrinter nodePrinter) { + this.printers.put(nodeClass, nodePrinter); + } } diff --git a/src/main/java/graphql/language/NodeBuilder.java b/src/main/java/graphql/language/NodeBuilder.java index 878358d874..df88e16632 100644 --- a/src/main/java/graphql/language/NodeBuilder.java +++ b/src/main/java/graphql/language/NodeBuilder.java @@ -5,8 +5,6 @@ import java.util.List; import java.util.Map; -import static graphql.Assert.assertNotNull; - @PublicApi public interface NodeBuilder { diff --git a/src/main/java/graphql/language/ObjectTypeExtensionDefinition.java b/src/main/java/graphql/language/ObjectTypeExtensionDefinition.java index 400f70054e..575827564f 100644 --- a/src/main/java/graphql/language/ObjectTypeExtensionDefinition.java +++ b/src/main/java/graphql/language/ObjectTypeExtensionDefinition.java @@ -6,7 +6,6 @@ import graphql.PublicApi; import graphql.collect.ImmutableKit; -import java.util.ArrayList; import java.util.LinkedHashMap; import java.util.List; import java.util.Map; diff --git a/src/main/java/graphql/language/PrettyAstPrinter.java b/src/main/java/graphql/language/PrettyAstPrinter.java new file mode 100644 index 0000000000..02a06bf004 --- /dev/null +++ b/src/main/java/graphql/language/PrettyAstPrinter.java @@ -0,0 +1,465 @@ +package graphql.language; + +import graphql.ExperimentalApi; +import graphql.collect.ImmutableKit; +import graphql.parser.CommentParser; +import graphql.parser.NodeToRuleCapturingParser; +import graphql.parser.ParserEnvironment; + +import java.util.Collections; +import java.util.List; +import java.util.Objects; +import java.util.Optional; +import java.util.function.Function; +import java.util.stream.Collectors; + +import static graphql.Assert.assertTrue; +import static graphql.parser.ParserEnvironment.newParserEnvironment; + +/** + * A printer that acts as a code formatter. + * + * This printer will preserve pretty much all elements from the source text, even those that are not part of the AST + * (and are thus discarded by the {@link AstPrinter}), like comments. + * + * @see AstPrinter + */ +@ExperimentalApi +public class PrettyAstPrinter extends AstPrinter { + private final CommentParser commentParser; + private final PrettyPrinterOptions options; + + public PrettyAstPrinter(NodeToRuleCapturingParser.ParserContext parserContext) { + this(parserContext, PrettyPrinterOptions.defaultOptions); + } + + public PrettyAstPrinter(NodeToRuleCapturingParser.ParserContext parserContext, PrettyPrinterOptions options) { + super(false); + this.commentParser = new CommentParser(parserContext); + this.options = options; + + this.replacePrinter(DirectiveDefinition.class, directiveDefinition()); + this.replacePrinter(Document.class, document()); + this.replacePrinter(EnumTypeDefinition.class, enumTypeDefinition("enum")); + this.replacePrinter(EnumTypeExtensionDefinition.class, enumTypeDefinition("extend enum")); + this.replacePrinter(EnumValueDefinition.class, enumValueDefinition()); + this.replacePrinter(FieldDefinition.class, fieldDefinition()); + this.replacePrinter(InputObjectTypeDefinition.class, inputObjectTypeDefinition("input")); + this.replacePrinter(InputObjectTypeExtensionDefinition.class, inputObjectTypeDefinition("extend input")); + this.replacePrinter(InputValueDefinition.class, inputValueDefinition()); + this.replacePrinter(InterfaceTypeDefinition.class, implementingTypeDefinition("interface")); + this.replacePrinter(InterfaceTypeExtensionDefinition.class, implementingTypeDefinition("extend interface")); + this.replacePrinter(ObjectTypeDefinition.class, implementingTypeDefinition("type")); + this.replacePrinter(ObjectTypeExtensionDefinition.class, implementingTypeDefinition("extend type")); + this.replacePrinter(ScalarTypeDefinition.class, scalarTypeDefinition("scalar")); + this.replacePrinter(ScalarTypeExtensionDefinition.class, scalarTypeDefinition("extend scalar")); + this.replacePrinter(UnionTypeDefinition.class, unionTypeDefinition("union")); + this.replacePrinter(UnionTypeExtensionDefinition.class, unionTypeDefinition("extend union")); + } + + public String print(Node node) { + StringBuilder builder = new StringBuilder(); + + NodePrinter nodePrinter = this._findPrinter(node); + nodePrinter.print(builder, node); + + return builder.toString(); + } + + public static String print(String schemaDefinition, PrettyPrinterOptions options) { + NodeToRuleCapturingParser parser = new NodeToRuleCapturingParser(); + ParserEnvironment parserEnvironment = newParserEnvironment().document(schemaDefinition).build(); + Document document = parser.parseDocument(parserEnvironment); + + return new PrettyAstPrinter(parser.getParserContext(), options).print(document); + } + + private NodePrinter document() { + return (out, node) -> { + String firstLineComment = commentParser.getCommentOnFirstLineOfDocument(node) + .map(this::comment) + .map(append("\n")) + .orElse(""); + + out.append(firstLineComment); + out.append(join(node.getDefinitions(), "\n\n")).append("\n"); + + String endComments = comments(commentParser.getCommentsAfterAllDefinitions(node), "\n"); + + out.append(endComments); + }; + } + + private NodePrinter directiveDefinition() { + return (out, node) -> { + out.append(outset(node)); + String locations = join(node.getDirectiveLocations(), " | "); + String repeatable = node.isRepeatable() ? "repeatable " : ""; + out.append("directive @") + .append(node.getName()) + .append(block(node.getInputValueDefinitions(), node, "(", ")", "\n", ", ", "")) + .append(" ") + .append(repeatable) + .append("on ") + .append(locations); + }; + } + + private NodePrinter enumTypeDefinition(String nodeName) { + return (out, node) -> { + out.append(outset(node)); + out.append(spaced( + nodeName, + node.getName(), + directives(node.getDirectives()), + block(node.getEnumValueDefinitions(), node, "{", "}", "\n", null, null) + )); + }; + } + + private NodePrinter enumValueDefinition() { + return (out, node) -> { + out.append(outset(node)); + out.append(spaced( + node.getName(), + directives(node.getDirectives()) + )); + }; + } + + private NodePrinter fieldDefinition() { + return (out, node) -> { + out.append(outset(node)); + out.append(node.getName()) + .append(block(node.getInputValueDefinitions(), node, "(", ")", "\n", ", ", "")) + .append(": ") + .append(spaced( + type(node.getType()), + directives(node.getDirectives()) + )); + }; + } + + private String type(Type type) { + if (type instanceof NonNullType) { + NonNullType inner = (NonNullType) type; + return wrap("", type(inner.getType()), "!"); + } else if (type instanceof ListType) { + ListType inner = (ListType) type; + return wrap("[", type(inner.getType()), "]"); + } else { + TypeName inner = (TypeName) type; + return inner.getName(); + } + } + + private NodePrinter inputObjectTypeDefinition(String nodeName) { + return (out, node) -> { + out.append(outset(node)); + out.append(spaced( + nodeName, + node.getName(), + directives(node.getDirectives()), + block(node.getInputValueDefinitions(), node, "{", "}", "\n", null, null) + )); + }; + } + + private NodePrinter inputValueDefinition() { + String nameTypeSep = ": "; + String defaultValueEquals = "= "; + return (out, node) -> { + Value defaultValue = node.getDefaultValue(); + out.append(outset(node)); + out.append(spaced( + node.getName() + nameTypeSep + type(node.getType()), + wrap(defaultValueEquals, defaultValue, ""), + directives(node.getDirectives()) + )); + }; + } + + + private > NodePrinter implementingTypeDefinition(String nodeName) { + return (out, node) -> { + out.append(outset(node)); + out.append(spaced( + nodeName, + node.getName(), + wrap("implements ", block(node.getImplements(), node, "", "", " &\n", " & ", ""), ""), + directives(node.getDirectives()), + block(node.getFieldDefinitions(), node, "{", "}", "\n", null, null) + )); + }; + } + + private NodePrinter scalarTypeDefinition(String nodeName) { + return (out, node) -> { + out.append(outset(node)); + out.append(spaced( + nodeName, + node.getName(), + directives(node.getDirectives()))); + }; + } + + private NodePrinter unionTypeDefinition(String nodeName) { + String barSep = " | "; + String equals = "= "; + return (out, node) -> { + out.append(outset(node)); + out.append(spaced( + nodeName, + node.getName(), + directives(node.getDirectives()), + equals + join(node.getMemberTypes(), barSep) + )); + }; + } + + private String node(Node node, Class startClass) { + if (startClass != null) { + assertTrue(startClass.isInstance(node), () -> "The starting class must be in the inherit tree"); + } + StringBuilder builder = new StringBuilder(); + + String comments = comments(commentParser.getLeadingComments(node), "\n"); + builder.append(comments); + + NodePrinter printer = _findPrinter(node, startClass); + printer.print(builder, node); + + commentParser.getTrailingComment(node) + .map(this::comment) + .map(prepend(" ")) + .ifPresent(builder::append); + + return builder.toString(); + } + + private boolean isEmpty(List list) { + return list == null || list.isEmpty(); + } + + private boolean isEmpty(String s) { + return s == null || s.trim().length() == 0; + } + + private List nvl(List list) { + return list != null ? list : ImmutableKit.emptyList(); + } + + // Description and comments positioned before the node + private String outset(Node node) { + String description = description(node); + String commentsAfter = comments(commentParser.getCommentsAfterDescription(node), "\n"); + + return description + commentsAfter; + } + + private String description(Node node) { + Description description = ((AbstractDescribedNode) node).getDescription(); + if (description == null || description.getContent() == null) { + return ""; + } + String s; + boolean startNewLine = description.getContent().length() > 0 && description.getContent().charAt(0) == '\n'; + if (description.isMultiLine()) { + s = "\"\"\"" + (startNewLine ? "" : "\n") + description.getContent() + "\n\"\"\"\n"; + } else { + s = "\"" + description.getContent() + "\"\n"; + } + return s; + } + + private String comment(Comment comment) { + return comments(Collections.singletonList(comment)); + } + + private String comments(List comments) { + return comments(comments, ""); + } + + private String comments(List comments, String suffix) { + return comments(comments, "", suffix); + } + + private String comments(List comments, String prefix, String suffix) { + if (comments.isEmpty()) { + return ""; + } + + return comments.stream() + .map(Comment::getContent) + .map(content -> "#" + content) + .collect(Collectors.joining("\n", prefix, suffix)); + } + + private String directives(List directives) { + return join(nvl(directives), " "); + } + + private String join(List nodes, String delim) { + return join(nodes, delim, "", ""); + } + + private String join(List nodes, String delim, String prefix, String suffix) { + StringBuilder joined = new StringBuilder(); + + joined.append(prefix); + boolean first = true; + for (T node : nodes) { + if (first) { + first = false; + } else { + joined.append(delim); + } + joined.append(node(node)); + } + + joined.append(suffix); + return joined.toString(); + } + + private String node(Node node) { + return node(node, null); + } + + private String spaced(String... args) { + return join(" ", args); + } + + private Function prepend(String prefix) { + return text -> prefix + text; + } + + private Function append(String suffix) { + return text -> text + suffix; + } + + private String join(String delim, String... args) { + StringBuilder builder = new StringBuilder(); + + boolean first = true; + for (final String arg : args) { + if (isEmpty(arg)) { + continue; + } + if (first) { + first = false; + } else { + builder.append(delim); + } + builder.append(arg); + } + + return builder.toString(); + } + + private String block(List nodes, Node parentNode, String prefix, String suffix, String separatorMultiline, String separatorSingleLine, String whenEmpty) { + if (isEmpty(nodes)) { + return whenEmpty != null ? whenEmpty : prefix + suffix; + } + + boolean hasDescriptions = nodes.stream() + .filter(node -> node instanceof AbstractDescribedNode) + .map(node -> (AbstractDescribedNode) node) + .map(AbstractDescribedNode::getDescription) + .anyMatch(Objects::nonNull); + + boolean hasTrailingComments = nodes.stream() + .map(commentParser::getTrailingComment) + .anyMatch(Optional::isPresent); + + boolean hasLeadingComments = nodes.stream() + .mapToLong(node -> commentParser.getLeadingComments(node).size()) + .sum() > 0; + + boolean isMultiline = hasDescriptions || hasTrailingComments || hasLeadingComments || separatorSingleLine == null; + + String appliedSeparator = isMultiline ? separatorMultiline : separatorSingleLine; + + String blockStart = commentParser.getBeginningOfBlockComment(parentNode, prefix) + .map(this::comment) + .map(commentText -> String.format("%s %s\n", prefix, commentText)) + .orElse(String.format("%s%s", prefix, (isMultiline ? "\n" : ""))); + + String blockEndComments = comments(commentParser.getEndOfBlockComments(parentNode, suffix), "\n", ""); + String blockEnd = (isMultiline ? "\n" : "") + suffix; + + String content = nodes.stream().map(this::node).collect(Collectors.joining(appliedSeparator)); + String possiblyIndentedContent = isMultiline ? indent(content + blockEndComments) : content + blockEndComments; + + return blockStart + possiblyIndentedContent + blockEnd; + } + + private String indent(String text) { + return indent(new StringBuilder(text)).toString(); + } + + private StringBuilder indent(StringBuilder stringBuilder) { + final String indentText = options.indentText; + + for (int i = 0; i < stringBuilder.length(); i++) { + char c = stringBuilder.charAt(i); + if (i == 0) { + stringBuilder.replace(i, i, indentText); + i += 2; + } + if (c == '\n') { + stringBuilder.replace(i, i + 1, "\n" + indentText); + i += 3; + } + } + return stringBuilder; + } + + /** + * Contains options that modify how a document is printed. + */ + public static class PrettyPrinterOptions { + private final String indentText; + private static final PrettyPrinterOptions defaultOptions = new PrettyPrinterOptions(IndentType.SPACE, 2); + + private PrettyPrinterOptions(IndentType indentType, int indentWidth) { + this.indentText = String.join("", Collections.nCopies(indentWidth, indentType.character)); + } + + public static PrettyPrinterOptions defaultOptions() { + return defaultOptions; + } + + public static Builder builder() { + return new Builder(); + } + + public enum IndentType { + TAB("\t"), SPACE(" "); + + private final String character; + + IndentType(String character) { + this.character = character; + } + } + + public static class Builder { + private IndentType indentType; + private int indentWidth = 1; + + public Builder indentType(IndentType indentType) { + this.indentType = indentType; + return this; + } + + public Builder indentWith(int indentWidth) { + this.indentWidth = indentWidth; + return this; + } + + public PrettyPrinterOptions build() { + return new PrettyPrinterOptions(indentType, indentWidth); + } + } + } +} diff --git a/src/main/java/graphql/language/TypeName.java b/src/main/java/graphql/language/TypeName.java index 313a0b9abb..add06add41 100644 --- a/src/main/java/graphql/language/TypeName.java +++ b/src/main/java/graphql/language/TypeName.java @@ -7,7 +7,6 @@ import graphql.util.TraversalControl; import graphql.util.TraverserContext; -import java.util.ArrayList; import java.util.LinkedHashMap; import java.util.List; import java.util.Map; diff --git a/src/main/java/graphql/language/VariableReference.java b/src/main/java/graphql/language/VariableReference.java index 14555402bb..ac085c694b 100644 --- a/src/main/java/graphql/language/VariableReference.java +++ b/src/main/java/graphql/language/VariableReference.java @@ -7,7 +7,6 @@ import graphql.util.TraversalControl; import graphql.util.TraverserContext; -import java.util.ArrayList; import java.util.LinkedHashMap; import java.util.List; import java.util.Map; diff --git a/src/main/java/graphql/nextgen/GraphQL.java b/src/main/java/graphql/nextgen/GraphQL.java deleted file mode 100644 index dc65416124..0000000000 --- a/src/main/java/graphql/nextgen/GraphQL.java +++ /dev/null @@ -1,380 +0,0 @@ -package graphql.nextgen; - -import graphql.ExecutionInput; -import graphql.ExecutionResult; -import graphql.ExecutionResultImpl; -import graphql.Internal; -import graphql.ParseAndValidate; -import graphql.ParseAndValidateResult; -import graphql.execution.AbortExecutionException; -import graphql.execution.ExecutionId; -import graphql.execution.ExecutionIdProvider; -import graphql.execution.instrumentation.DocumentAndVariables; -import graphql.execution.instrumentation.InstrumentationContext; -import graphql.execution.instrumentation.InstrumentationState; -import graphql.execution.instrumentation.nextgen.Instrumentation; -import graphql.execution.instrumentation.nextgen.InstrumentationCreateStateParameters; -import graphql.execution.instrumentation.nextgen.InstrumentationExecutionParameters; -import graphql.execution.instrumentation.nextgen.InstrumentationValidationParameters; -import graphql.execution.nextgen.DefaultExecutionStrategy; -import graphql.execution.nextgen.Execution; -import graphql.execution.nextgen.ExecutionStrategy; -import graphql.execution.preparsed.NoOpPreparsedDocumentProvider; -import graphql.execution.preparsed.PreparsedDocumentEntry; -import graphql.execution.preparsed.PreparsedDocumentProvider; -import graphql.language.Document; -import graphql.schema.GraphQLSchema; -import graphql.util.LogKit; -import graphql.validation.ValidationError; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -import java.util.List; -import java.util.concurrent.CompletableFuture; -import java.util.concurrent.atomic.AtomicReference; -import java.util.function.Consumer; -import java.util.function.Function; -import java.util.function.Predicate; -import java.util.function.UnaryOperator; - -import static graphql.Assert.assertNotNull; - -/** - * - * @deprecated Jan 2022 - We have decided to deprecate the NextGen engine, and it will be removed in a future release. - */ -@Deprecated -@SuppressWarnings("Duplicates") -@Internal -public class GraphQL { - private static final Logger log = LoggerFactory.getLogger(graphql.GraphQL.class); - private static final Logger logNotSafe = LogKit.getNotPrivacySafeLogger(ExecutionStrategy.class); - - private final GraphQLSchema graphQLSchema; - private final ExecutionStrategy executionStrategy; - private final ExecutionIdProvider idProvider; - private final Instrumentation instrumentation; - private final PreparsedDocumentProvider preparsedDocumentProvider; - - public GraphQL(Builder builder) { - this.graphQLSchema = builder.graphQLSchema; - this.executionStrategy = builder.executionStrategy; - this.idProvider = builder.idProvider; - this.preparsedDocumentProvider = builder.preparsedDocumentProvider; - this.instrumentation = builder.instrumentation; - } - - /** - * Executes the graphql query using the provided input object builder - *

- * This will return a completed {@link ExecutionResult} - * which is the result of executing the provided query. - * - * @param executionInputBuilder {@link ExecutionInput.Builder} - * - * @return an {@link ExecutionResult} which can include errors - */ - public ExecutionResult execute(ExecutionInput.Builder executionInputBuilder) { - return executeAsync(executionInputBuilder.build()).join(); - } - - /** - * Executes the graphql query using the provided input object builder - *

- * This will return a completed {@link ExecutionResult} - * which is the result of executing the provided query. - *

- * This allows a lambda style like : - *

-     * {@code
-     *    ExecutionResult result = graphql.execute(input -> input.query("{hello}").root(startingObj).context(contextObj));
-     * }
-     * 
- * - * @param builderFunction a function that is given a {@link ExecutionInput.Builder} - * - * @return a promise to an {@link ExecutionResult} which can include errors - */ - public CompletableFuture execute(UnaryOperator builderFunction) { - return executeAsync(builderFunction.apply(ExecutionInput.newExecutionInput()).build()); - } - - /** - * Executes the graphql query using the provided input object - *

- * This will return a completed {@link ExecutionResult} - * which is the result of executing the provided query. - * - * @param executionInput {@link ExecutionInput} - * - * @return a promise to an {@link ExecutionResult} which can include errors - */ - public ExecutionResult execute(ExecutionInput executionInput) { - return executeAsync(executionInput).join(); - } - - /** - * Executes the graphql query using the provided input object builder - *

- * This will return a promise (aka {@link CompletableFuture}) to provide a {@link ExecutionResult} - * which is the result of executing the provided query. - * - * @param executionInputBuilder {@link ExecutionInput.Builder} - * - * @return a promise to an {@link ExecutionResult} which can include errors - */ - public CompletableFuture executeAsync(ExecutionInput.Builder executionInputBuilder) { - return executeAsync(executionInputBuilder.build()); - } - - /** - * Executes the graphql query using the provided input object builder - *

- * This will return a promise (aka {@link CompletableFuture}) to provide a {@link ExecutionResult} - * which is the result of executing the provided query. - *

- * This allows a lambda style like : - *

-     * {@code
-     *    ExecutionResult result = graphql.executeAsync(input -> input.query("{hello}").root(startingObj).context(contextObj));
-     * }
-     * 
- * - * @param builderFunction a function that is given a {@link ExecutionInput.Builder} - * - * @return a promise to an {@link ExecutionResult} which can include errors - */ - public CompletableFuture executeAsync(UnaryOperator builderFunction) { - return executeAsync(builderFunction.apply(ExecutionInput.newExecutionInput()).build()); - } - - /** - * Executes the graphql query using the provided input object - *

- * This will return a promise (aka {@link CompletableFuture}) to provide a {@link ExecutionResult} - * which is the result of executing the provided query. - * - * @param executionInput {@link ExecutionInput} - * - * @return a promise to an {@link ExecutionResult} which can include errors - */ - public CompletableFuture executeAsync(ExecutionInput executionInput) { - try { - if (logNotSafe.isDebugEnabled()) { - logNotSafe.debug("Executing request. operation name: '{}'. query: '{}'. variables '{}'", executionInput.getOperationName(), executionInput.getQuery(), executionInput.getVariables()); - } - - InstrumentationState instrumentationState = instrumentation.createState(new InstrumentationCreateStateParameters(this.graphQLSchema, executionInput)); - - InstrumentationExecutionParameters inputInstrumentationParameters = new InstrumentationExecutionParameters(executionInput, this.graphQLSchema, instrumentationState); - executionInput = instrumentation.instrumentExecutionInput(executionInput, inputInstrumentationParameters); - - InstrumentationExecutionParameters instrumentationParameters = new InstrumentationExecutionParameters(executionInput, this.graphQLSchema, instrumentationState); - InstrumentationContext executionInstrumentation = instrumentation.beginExecution(instrumentationParameters); - - GraphQLSchema graphQLSchema = instrumentation.instrumentSchema(this.graphQLSchema, instrumentationParameters); - - CompletableFuture executionResult = parseValidateAndExecute(executionInput, graphQLSchema, instrumentationState); - // - // finish up instrumentation - executionResult = executionResult.whenComplete(executionInstrumentation::onCompleted); - // - // allow instrumentation to tweak the result - executionResult = executionResult.thenApply(result -> instrumentation.instrumentExecutionResult(result, instrumentationParameters)); - return executionResult; - } catch (AbortExecutionException abortException) { - return CompletableFuture.completedFuture(abortException.toExecutionResult()); - } - } - - private CompletableFuture parseValidateAndExecute(ExecutionInput executionInput, GraphQLSchema graphQLSchema, InstrumentationState instrumentationState) { - AtomicReference executionInputRef = new AtomicReference<>(executionInput); - Function computeFunction = transformedInput -> { - // if they change the original query in the pre-parser, then we want to see it downstream from then on - executionInputRef.set(transformedInput); - return parseAndValidate(executionInputRef, graphQLSchema, instrumentationState); - }; - CompletableFuture preparsedDoc = preparsedDocumentProvider.getDocumentAsync(executionInput, computeFunction); - return preparsedDoc.thenCompose(preparsedDocumentEntry -> { - if (preparsedDocumentEntry.hasErrors()) { - return CompletableFuture.completedFuture(new ExecutionResultImpl(preparsedDocumentEntry.getErrors())); - } - try { - return execute(executionInputRef.get(), preparsedDocumentEntry.getDocument(), graphQLSchema, instrumentationState); - } catch (AbortExecutionException e) { - return CompletableFuture.completedFuture(e.toExecutionResult()); - } - }); - } - - private PreparsedDocumentEntry parseAndValidate(AtomicReference executionInputRef, GraphQLSchema graphQLSchema, InstrumentationState instrumentationState) { - - ExecutionInput executionInput = executionInputRef.get(); - String query = executionInput.getQuery(); - - if (logNotSafe.isDebugEnabled()) { - logNotSafe.debug("Parsing query: '{}'...", query); - } - ParseAndValidateResult parseResult = parse(executionInput, graphQLSchema, instrumentationState); - if (parseResult.isFailure()) { - logNotSafe.warn("Query did not parse : '{}'", executionInput.getQuery()); - return new PreparsedDocumentEntry(parseResult.getSyntaxException().toInvalidSyntaxError()); - } else { - final Document document = parseResult.getDocument(); - // they may have changed the document and the variables via instrumentation so update the reference to it - executionInput = executionInput.transform(builder -> builder.variables(parseResult.getVariables())); - executionInputRef.set(executionInput); - - if (logNotSafe.isDebugEnabled()) { - logNotSafe.debug("Validating query: '{}'", query); - } - final List errors = validate(executionInput, document, graphQLSchema, instrumentationState); - if (!errors.isEmpty()) { - logNotSafe.warn("Query did not validate : '{}'", query); - return new PreparsedDocumentEntry(document, errors); - } - - return new PreparsedDocumentEntry(document); - } - } - - private ParseAndValidateResult parse(ExecutionInput executionInput, GraphQLSchema graphQLSchema, InstrumentationState instrumentationState) { - InstrumentationExecutionParameters parameters = new InstrumentationExecutionParameters(executionInput, graphQLSchema, instrumentationState); - InstrumentationContext parseInstrumentation = instrumentation.beginParse(parameters); - CompletableFuture documentCF = new CompletableFuture<>(); - parseInstrumentation.onDispatched(documentCF); - - ParseAndValidateResult parseResult = ParseAndValidate.parse(executionInput); - if (parseResult.isFailure()) { - parseInstrumentation.onCompleted(null, parseResult.getSyntaxException()); - return parseResult; - } else { - documentCF.complete(parseResult.getDocument()); - parseInstrumentation.onCompleted(parseResult.getDocument(), null); - - DocumentAndVariables documentAndVariables = parseResult.getDocumentAndVariables(); - documentAndVariables = instrumentation.instrumentDocumentAndVariables(documentAndVariables, parameters); - return ParseAndValidateResult.newResult() - .document(documentAndVariables.getDocument()).variables(documentAndVariables.getVariables()).build(); - } - } - - private List validate(ExecutionInput executionInput, Document document, GraphQLSchema graphQLSchema, InstrumentationState instrumentationState) { - InstrumentationContext> validationCtx = instrumentation.beginValidation(new InstrumentationValidationParameters(executionInput, document, graphQLSchema, instrumentationState)); - CompletableFuture> cf = new CompletableFuture<>(); - validationCtx.onDispatched(cf); - - Predicate> validationRulePredicate = executionInput.getGraphQLContext().getOrDefault(ParseAndValidate.INTERNAL_VALIDATION_PREDICATE_HINT, r -> true); - List validationErrors = ParseAndValidate.validate(graphQLSchema, document, validationRulePredicate, executionInput.getLocale()); - - validationCtx.onCompleted(validationErrors, null); - cf.complete(validationErrors); - return validationErrors; - } - - private CompletableFuture execute(ExecutionInput executionInput, Document document, GraphQLSchema graphQLSchema, InstrumentationState instrumentationState) { - String query = executionInput.getQuery(); - String operationName = executionInput.getOperationName(); - Object context = executionInput.getGraphQLContext(); - - Execution execution = new Execution(); - ExecutionId executionId = idProvider.provide(query, operationName, context); - - if (logNotSafe.isDebugEnabled()) { - logNotSafe.debug("Executing '{}'. operation name: '{}'. query: '{}'. variables '{}'", executionId, executionInput.getOperationName(), executionInput.getQuery(), executionInput.getVariables()); - } - CompletableFuture future = execution.execute(executionStrategy, document, graphQLSchema, executionId, executionInput, instrumentationState); - future = future.whenComplete((result, throwable) -> { - if (throwable != null) { - log.error(String.format("Execution '%s' threw exception when executing : query : '%s'. variables '%s'", executionId, executionInput.getQuery(), executionInput.getVariables()), throwable); - } else { - if (log.isDebugEnabled()) { - int errorCount = result.getErrors().size(); - if (errorCount > 0) { - log.debug("Execution '{}' completed with '{}' errors", executionId, errorCount); - } else { - log.debug("Execution '{}' completed with zero errors", executionId); - } - } - } - }); - return future; - } - - /** - * Helps you build a GraphQL object ready to execute queries - * - * @param graphQLSchema the schema to use - * - * @return a builder of GraphQL objects - */ - public static Builder newGraphQL(GraphQLSchema graphQLSchema) { - return new Builder(graphQLSchema); - } - - /** - * This helps you transform the current GraphQL object into another one by starting a builder with all - * the current values and allows you to transform it how you want. - * - * @param builderConsumer the consumer code that will be given a builder to transform - * - * @return a new GraphQL object based on calling build on that builder - */ - public GraphQL transform(Consumer builderConsumer) { - Builder builder = new Builder(this); - builderConsumer.accept(builder); - return builder.build(); - } - - - public static class Builder { - private GraphQLSchema graphQLSchema; - private ExecutionStrategy executionStrategy = new DefaultExecutionStrategy(); - private ExecutionIdProvider idProvider = ExecutionIdProvider.DEFAULT_EXECUTION_ID_PROVIDER; - private Instrumentation instrumentation = new Instrumentation() { - }; - private PreparsedDocumentProvider preparsedDocumentProvider = NoOpPreparsedDocumentProvider.INSTANCE; - - - public Builder(GraphQLSchema graphQLSchema) { - this.graphQLSchema = graphQLSchema; - } - - public Builder(GraphQL graphQL) { - this.graphQLSchema = graphQL.graphQLSchema; - this.executionStrategy = graphQL.executionStrategy; - this.idProvider = graphQL.idProvider; - this.instrumentation = graphQL.instrumentation; - } - - public Builder schema(GraphQLSchema graphQLSchema) { - this.graphQLSchema = assertNotNull(graphQLSchema, () -> "GraphQLSchema must be non null"); - return this; - } - - public Builder executionStrategy(ExecutionStrategy executionStrategy) { - this.executionStrategy = assertNotNull(executionStrategy, () -> "ExecutionStrategy must be non null"); - return this; - } - - public Builder instrumentation(Instrumentation instrumentation) { - this.instrumentation = assertNotNull(instrumentation, () -> "Instrumentation must be non null"); - return this; - } - - public Builder preparsedDocumentProvider(PreparsedDocumentProvider preparsedDocumentProvider) { - this.preparsedDocumentProvider = assertNotNull(preparsedDocumentProvider, () -> "PreparsedDocumentProvider must be non null"); - return this; - } - - public Builder executionIdProvider(ExecutionIdProvider executionIdProvider) { - this.idProvider = assertNotNull(executionIdProvider, () -> "ExecutionIdProvider must be non null"); - return this; - } - - public GraphQL build() { - assertNotNull(graphQLSchema, () -> "graphQLSchema must be non null"); - return new GraphQL(this); - } - } -} diff --git a/src/main/java/graphql/nextgen/package-info.java b/src/main/java/graphql/nextgen/package-info.java deleted file mode 100644 index 332290a018..0000000000 --- a/src/main/java/graphql/nextgen/package-info.java +++ /dev/null @@ -1,10 +0,0 @@ -/** - * WARNING: All code in this package is a work in progress for a new execution engine. - * - *Jan 2022 - We have decided to deprecate the NextGen engine, and it will be removed in a future release. - */ -@Internal -@Deprecated -package graphql.nextgen; - -import graphql.Internal; \ No newline at end of file diff --git a/src/main/java/graphql/normalized/ExecutableNormalizedField.java b/src/main/java/graphql/normalized/ExecutableNormalizedField.java index d0e62c6f7a..98e527811f 100644 --- a/src/main/java/graphql/normalized/ExecutableNormalizedField.java +++ b/src/main/java/graphql/normalized/ExecutableNormalizedField.java @@ -28,12 +28,14 @@ import java.util.Map; import java.util.Set; import java.util.function.Consumer; -import java.util.stream.Collectors; import static graphql.Assert.assertNotNull; import static graphql.Assert.assertTrue; import static graphql.schema.GraphQLTypeUtil.simplePrint; import static graphql.schema.GraphQLTypeUtil.unwrapAll; +import static java.util.stream.Collectors.joining; +import static java.util.stream.Collectors.toList; +import static java.util.stream.Collectors.toSet; /** * Intentionally Mutable @@ -193,7 +195,7 @@ public boolean hasChildren() { public GraphQLOutputType getType(GraphQLSchema schema) { List fieldDefinitions = getFieldDefinitions(schema); - Set fieldTypes = fieldDefinitions.stream().map(fd -> simplePrint(fd.getType())).collect(Collectors.toSet()); + Set fieldTypes = fieldDefinitions.stream().map(fd -> simplePrint(fd.getType())).collect(toSet()); Assert.assertTrue(fieldTypes.size() == 1, () -> "More than one type ... use getTypes"); return fieldDefinitions.get(0).getType(); } @@ -355,6 +357,29 @@ public List getChildrenWithSameResultKey(String resul return FpKit.filterList(children, child -> child.getResultKey().equals(resultKey)); } + public List getChildren(int includingRelativeLevel) { + List result = new ArrayList<>(); + assertTrue(includingRelativeLevel >= 1, () -> "relative level must be >= 1"); + + this.getChildren().forEach(child -> { + traverseImpl(child, result::add, 1, includingRelativeLevel); + }); + return result; + } + + /** + * This returns the child fields that can be used if the object is of the specified object type + * + * @param objectTypeName the object type + * + * @return a list of child fields that would apply to that object type + */ + public List getChildren(String objectTypeName) { + return children.stream() + .filter(cld -> cld.objectTypeNames.contains(objectTypeName)) + .collect(toList()); + } + public int getLevel() { return level; } @@ -374,19 +399,10 @@ public String toString() { objectTypeNamesToString() + "." + fieldName + ", alias=" + alias + ", level=" + level + - ", children=" + children.stream().map(ExecutableNormalizedField::toString).collect(Collectors.joining("\n")) + + ", children=" + children.stream().map(ExecutableNormalizedField::toString).collect(joining("\n")) + '}'; } - public List getChildren(int includingRelativeLevel) { - List result = new ArrayList<>(); - assertTrue(includingRelativeLevel >= 1, () -> "relative level must be >= 1"); - - this.getChildren().forEach(child -> { - traverseImpl(child, result::add, 1, includingRelativeLevel); - }); - return result; - } public void traverseSubTree(Consumer consumer) { this.getChildren().forEach(child -> { diff --git a/src/main/java/graphql/normalized/ExecutableNormalizedOperationFactory.java b/src/main/java/graphql/normalized/ExecutableNormalizedOperationFactory.java index 1dad8296f9..6093fb0967 100644 --- a/src/main/java/graphql/normalized/ExecutableNormalizedOperationFactory.java +++ b/src/main/java/graphql/normalized/ExecutableNormalizedOperationFactory.java @@ -4,6 +4,7 @@ import com.google.common.collect.ImmutableListMultimap; import com.google.common.collect.ImmutableMap; import com.google.common.collect.ImmutableSet; +import graphql.GraphQLContext; import graphql.Internal; import graphql.collect.ImmutableKit; import graphql.execution.CoercedVariables; @@ -11,7 +12,6 @@ import graphql.execution.MergedField; import graphql.execution.RawVariables; import graphql.execution.ValuesResolver; -import graphql.execution.nextgen.Common; import graphql.introspection.Introspection; import graphql.language.Document; import graphql.language.Field; @@ -33,12 +33,14 @@ import graphql.schema.GraphQLTypeUtil; import graphql.schema.GraphQLUnionType; import graphql.schema.GraphQLUnmodifiedType; +import graphql.schema.impl.SchemaUtil; import org.jetbrains.annotations.Nullable; import java.util.ArrayList; import java.util.Collection; import java.util.LinkedHashMap; import java.util.List; +import java.util.Locale; import java.util.Map; import java.util.Set; @@ -77,19 +79,29 @@ public static ExecutableNormalizedOperation createExecutableNormalizedOperationW Document document, String operationName, RawVariables rawVariables) { + return createExecutableNormalizedOperationWithRawVariables(graphQLSchema, document, operationName, rawVariables, GraphQLContext.getDefault(), Locale.getDefault()); + } + + public static ExecutableNormalizedOperation createExecutableNormalizedOperationWithRawVariables(GraphQLSchema graphQLSchema, + Document document, + String operationName, + RawVariables rawVariables, + GraphQLContext graphQLContext, + Locale locale) { NodeUtil.GetOperationResult getOperationResult = NodeUtil.getOperation(document, operationName); - return new ExecutableNormalizedOperationFactory().createExecutableNormalizedOperationImplWithRawVariables(graphQLSchema, getOperationResult.operationDefinition, getOperationResult.fragmentsByName, rawVariables); + return new ExecutableNormalizedOperationFactory().createExecutableNormalizedOperationImplWithRawVariables(graphQLSchema, getOperationResult.operationDefinition, getOperationResult.fragmentsByName, rawVariables, graphQLContext, locale); } private ExecutableNormalizedOperation createExecutableNormalizedOperationImplWithRawVariables(GraphQLSchema graphQLSchema, OperationDefinition operationDefinition, Map fragments, - RawVariables rawVariables - ) { + RawVariables rawVariables, + GraphQLContext graphQLContext, + Locale locale) { List variableDefinitions = operationDefinition.getVariableDefinitions(); - CoercedVariables coercedVariableValues = ValuesResolver.coerceVariableValues(graphQLSchema, variableDefinitions, rawVariables); - Map normalizedVariableValues = ValuesResolver.getNormalizedVariableValues(graphQLSchema, variableDefinitions, rawVariables); + CoercedVariables coercedVariableValues = ValuesResolver.coerceVariableValues(graphQLSchema, variableDefinitions, rawVariables, graphQLContext, locale); + Map normalizedVariableValues = ValuesResolver.getNormalizedVariableValues(graphQLSchema, variableDefinitions, rawVariables, graphQLContext, locale); return createNormalizedQueryImpl(graphQLSchema, operationDefinition, fragments, coercedVariableValues, normalizedVariableValues); } @@ -109,7 +121,7 @@ private ExecutableNormalizedOperation createNormalizedQueryImpl(GraphQLSchema gr .normalizedVariables(normalizedVariableValues) .build(); - GraphQLObjectType rootType = Common.getOperationRootType(graphQLSchema, operationDefinition); + GraphQLObjectType rootType = SchemaUtil.getOperationRootType(graphQLSchema, operationDefinition); CollectNFResult collectFromOperationResult = collectFromOperation(parameters, operationDefinition, rootType); @@ -304,7 +316,7 @@ private ExecutableNormalizedField createNF(FieldCollectorNormalizedQueryParams p String fieldName = field.getName(); GraphQLFieldDefinition fieldDefinition = Introspection.getFieldDef(parameters.getGraphQLSchema(), objectTypes.iterator().next(), fieldName); - Map argumentValues = ValuesResolver.getArgumentValues(fieldDefinition.getArguments(), field.getArguments(),CoercedVariables.of(parameters.getCoercedVariableValues())); + Map argumentValues = ValuesResolver.getArgumentValues(fieldDefinition.getArguments(), field.getArguments(), CoercedVariables.of(parameters.getCoercedVariableValues()), parameters.getGraphQLContext(), parameters.getLocale()); Map normalizedArgumentValues = null; if (parameters.getNormalizedVariableValues() != null) { normalizedArgumentValues = ValuesResolver.getNormalizedArgumentValues(fieldDefinition.getArguments(), field.getArguments(), parameters.getNormalizedVariableValues()); diff --git a/src/main/java/graphql/normalized/FieldCollectorNormalizedQueryParams.java b/src/main/java/graphql/normalized/FieldCollectorNormalizedQueryParams.java index ba6004796c..f7e6675896 100644 --- a/src/main/java/graphql/normalized/FieldCollectorNormalizedQueryParams.java +++ b/src/main/java/graphql/normalized/FieldCollectorNormalizedQueryParams.java @@ -1,6 +1,7 @@ package graphql.normalized; import graphql.Assert; +import graphql.GraphQLContext; import graphql.Internal; import graphql.language.FragmentDefinition; import graphql.schema.GraphQLSchema; @@ -10,6 +11,7 @@ import java.util.ArrayList; import java.util.LinkedHashMap; import java.util.List; +import java.util.Locale; import java.util.Map; @Internal @@ -18,6 +20,8 @@ public class FieldCollectorNormalizedQueryParams { private final Map fragmentsByName; private final Map coercedVariableValues; private final Map normalizedVariableValues; + private final GraphQLContext graphQLContext; + private final Locale locale; public List possibleMergerList = new ArrayList<>(); @@ -53,14 +57,21 @@ public Map getNormalizedVariableValues() { return normalizedVariableValues; } - private FieldCollectorNormalizedQueryParams(GraphQLSchema graphQLSchema, - Map coercedVariableValues, - Map normalizedVariableValues, - Map fragmentsByName) { - this.fragmentsByName = fragmentsByName; - this.graphQLSchema = graphQLSchema; - this.coercedVariableValues = coercedVariableValues; - this.normalizedVariableValues = normalizedVariableValues; + public GraphQLContext getGraphQLContext() { + return graphQLContext; + } + + public Locale getLocale() { + return locale; + } + + private FieldCollectorNormalizedQueryParams(Builder builder) { + this.fragmentsByName = builder.fragmentsByName; + this.graphQLSchema = builder.graphQLSchema; + this.coercedVariableValues = builder.coercedVariableValues; + this.normalizedVariableValues = builder.normalizedVariableValues; + this.graphQLContext = builder.graphQLContext; + this.locale = builder.locale; } public static Builder newParameters() { @@ -72,6 +83,8 @@ public static class Builder { private final Map fragmentsByName = new LinkedHashMap<>(); private final Map coercedVariableValues = new LinkedHashMap<>(); private Map normalizedVariableValues; + private GraphQLContext graphQLContext = GraphQLContext.getDefault(); + private Locale locale = Locale.getDefault(); /** * @see FieldCollectorNormalizedQueryParams#newParameters() @@ -100,9 +113,19 @@ public Builder normalizedVariables(Map normalizedV return this; } + public Builder graphQLContext(GraphQLContext graphQLContext) { + this.graphQLContext = graphQLContext; + return this; + } + + public Builder locale(Locale locale) { + this.locale = locale; + return this; + } + public FieldCollectorNormalizedQueryParams build() { Assert.assertNotNull(graphQLSchema, () -> "You must provide a schema"); - return new FieldCollectorNormalizedQueryParams(graphQLSchema, coercedVariableValues, normalizedVariableValues, fragmentsByName); + return new FieldCollectorNormalizedQueryParams(this); } } diff --git a/src/main/java/graphql/parser/CommentParser.java b/src/main/java/graphql/parser/CommentParser.java new file mode 100644 index 0000000000..cb6e0f1a0d --- /dev/null +++ b/src/main/java/graphql/parser/CommentParser.java @@ -0,0 +1,247 @@ +package graphql.parser; + +import com.google.common.collect.ImmutableList; +import graphql.Internal; +import graphql.language.AbstractDescribedNode; +import graphql.language.Comment; +import graphql.language.Document; +import graphql.language.Node; +import org.antlr.v4.runtime.CommonTokenStream; +import org.antlr.v4.runtime.ParserRuleContext; +import org.antlr.v4.runtime.Token; + +import java.util.Collections; +import java.util.List; +import java.util.Map; +import java.util.Optional; +import java.util.function.Predicate; + +/** + * Contains methods for extracting {@link Comment} in various positions within and around {@link Node}s + */ +@Internal +public class CommentParser { + private final Map, ParserRuleContext> nodeToRuleMap; + private CommonTokenStream tokens; + private static final int CHANNEL_COMMENTS = 2; + + public CommentParser(NodeToRuleCapturingParser.ParserContext parserContext) { + nodeToRuleMap = parserContext.getNodeToRuleMap(); + tokens = parserContext.getTokens(); + } + + /* + * type MyType { # beginning of type block + * field( # beginning of field args block + * arg1: String + * arg2: String + * ) + * } + */ + public Optional getBeginningOfBlockComment(Node node, String prefix) { + final ParserRuleContext ctx = nodeToRuleMap.get(node); + + final Token start = ctx.start; + + if (start != null) { + return this.tokens.getTokens(start.getTokenIndex(), ctx.stop.getTokenIndex()).stream() + .filter(token -> token.getText().equals(prefix)) + .findFirst() + .map(token -> tokens.getHiddenTokensToRight(token.getTokenIndex(), CHANNEL_COMMENTS)) + .map(commentTokens -> getCommentOnChannel(commentTokens, isNotPrecededByLineBreak)) + .flatMap(comments -> comments.stream().findFirst()); + } + + return Optional.empty(); + } + + /* + * type MyType { + * a( + * arg1: A + * arg2: B + * # end of field args block comment + * ): A + * # end of type block comment #1 + * # end of type block comment #2 + * } + */ + public List getEndOfBlockComments(Node node, String blockSuffix) { + final ParserRuleContext ctx = nodeToRuleMap.get(node); + + return searchTokenToLeft(ctx.stop, blockSuffix) + .map(suffixToken -> tokens.getHiddenTokensToLeft(suffixToken.getTokenIndex(), CHANNEL_COMMENTS)) + .map(commentTokens -> getCommentOnChannel(commentTokens, isPrecededByLineBreak)) + .orElse(Collections.emptyList()); + } + + /* + * type MyType { + * a: A # field trailing comment + * } # type trailing comment + */ + public Optional getTrailingComment(Node node) { + // Only nodes that can hold descriptions can have trailing comments + if (!(node instanceof AbstractDescribedNode)) { + return Optional.empty(); + } + final ParserRuleContext ctx = nodeToRuleMap.get(node); + + List rightRefChannel = this.tokens.getHiddenTokensToRight(ctx.stop.getTokenIndex(), CHANNEL_COMMENTS); + + if (rightRefChannel != null) { + List comments = getCommentOnChannel(rightRefChannel, isNotPrecededByLineBreak); + + return comments.stream().findFirst(); + } + + return Optional.empty(); + } + + /* + * # type leading comment #1 + * # type leading comment #2 + * type MyType { + * # field leading comment #1 + * # field leading comment #2 + * a: A + * } + */ + public List getLeadingComments(Node node) { + final ParserRuleContext ctx = nodeToRuleMap.get(node); + + final Token start = ctx.start; + if (start != null) { + int tokPos = start.getTokenIndex(); + List leftRefChannel = this.tokens.getHiddenTokensToLeft(tokPos, CHANNEL_COMMENTS); + if (leftRefChannel != null) { + return getCommentOnChannel(leftRefChannel, isPrecededByLineBreak); + } + } + + return Collections.emptyList(); + } + + /* + * """ Description """ + * # comment after description #1 + * # comment after description #2 + * type MyType { + * a: A + * } + */ + public List getCommentsAfterDescription(Node node) { + // Early return if node doesn't have a description + if (!(node instanceof AbstractDescribedNode) || + (node instanceof AbstractDescribedNode && ((AbstractDescribedNode) node).getDescription() == null) + ) { + return Collections.emptyList(); + } + + final ParserRuleContext ctx = nodeToRuleMap.get(node); + + final Token start = ctx.start; + if (start != null) { + List commentTokens = tokens.getHiddenTokensToRight(start.getTokenIndex(), CHANNEL_COMMENTS); + + if (commentTokens != null) { + return getCommentOnChannel(commentTokens, isPrecededByLineBreak); + } + } + + return Collections.emptyList(); + } + + public Optional getCommentOnFirstLineOfDocument(Document node) { + final ParserRuleContext ctx = nodeToRuleMap.get(node); + + final Token start = ctx.start; + if (start != null) { + int tokPos = start.getTokenIndex(); + List leftRefChannel = this.tokens.getHiddenTokensToLeft(tokPos, CHANNEL_COMMENTS); + if (leftRefChannel != null) { + List comments = getCommentOnChannel(leftRefChannel, isFirstToken.or(isPrecededOnlyBySpaces)); + + return comments.stream().findFirst(); + } + } + + return Optional.empty(); + } + + public List getCommentsAfterAllDefinitions(Document node) { + final ParserRuleContext ctx = nodeToRuleMap.get(node); + + final Token start = ctx.start; + if (start != null) { + List leftRefChannel = this.tokens.getHiddenTokensToRight(ctx.stop.getTokenIndex(), CHANNEL_COMMENTS); + if (leftRefChannel != null) { + return getCommentOnChannel(leftRefChannel, + refToken -> Optional.ofNullable(this.tokens.getHiddenTokensToLeft(refToken.getTokenIndex(), -1)) + .map(hiddenTokens -> hiddenTokens.stream().anyMatch(token -> token.getText().equals("\n"))) + .orElse(false) + ); + } + } + + return Collections.emptyList(); + } + + protected List getCommentOnChannel(List refChannel, Predicate shouldIncludePredicate) { + ImmutableList.Builder comments = ImmutableList.builder(); + for (Token refTok : refChannel) { + String text = refTok.getText(); + // we strip the leading hash # character but we don't trim because we don't + // know the "comment markup". Maybe it's space sensitive, maybe it's not. So + // consumers can decide that + if (text == null) { + continue; + } + + boolean shouldIncludeComment = shouldIncludePredicate.test(refTok); + + if (!shouldIncludeComment) { + continue; + } + + text = text.replaceFirst("^#", ""); + + comments.add(new Comment(text, null)); + } + return comments.build(); + } + + private Optional searchTokenToLeft(Token token, String text) { + int i = token.getTokenIndex(); + + while (i > 0) { + Token t = tokens.get(i); + if (t.getText().equals(text)) { + return Optional.of(t); + } + i--; + } + + return Optional.empty(); + } + + private final Predicate alwaysTrue = token -> true; + + private final Predicate isNotPrecededByLineBreak = refToken -> + Optional.ofNullable(tokens.getHiddenTokensToLeft(refToken.getTokenIndex(), -1)) + .map(hiddenTokens -> hiddenTokens.stream().noneMatch(token -> token.getText().equals("\n"))) + .orElse(false); + + private final Predicate isPrecededByLineBreak = refToken -> + Optional.ofNullable(this.tokens.getHiddenTokensToLeft(refToken.getTokenIndex(), -1)) + .map(hiddenTokens -> hiddenTokens.stream().anyMatch(token -> token.getText().equals("\n"))) + .orElse(false); + + private final Predicate isFirstToken = refToken -> refToken.getTokenIndex() == 0; + + private final Predicate isPrecededOnlyBySpaces = refToken -> + Optional.ofNullable(this.tokens.getTokens(0, refToken.getTokenIndex() - 1)) + .map(beforeTokens -> beforeTokens.stream().allMatch(token -> token.getText().equals(" "))) + .orElse(false); + +} diff --git a/src/main/java/graphql/parser/ExtendedBailStrategy.java b/src/main/java/graphql/parser/ExtendedBailStrategy.java index e6d33d5ea9..a0861ed0c1 100644 --- a/src/main/java/graphql/parser/ExtendedBailStrategy.java +++ b/src/main/java/graphql/parser/ExtendedBailStrategy.java @@ -1,19 +1,25 @@ package graphql.parser; +import com.google.common.collect.ImmutableList; import graphql.Internal; import graphql.language.SourceLocation; +import graphql.parser.exceptions.MoreTokensSyntaxException; import org.antlr.v4.runtime.BailErrorStrategy; import org.antlr.v4.runtime.Parser; import org.antlr.v4.runtime.RecognitionException; import org.antlr.v4.runtime.Token; import org.antlr.v4.runtime.misc.ParseCancellationException; +import java.util.List; + @Internal public class ExtendedBailStrategy extends BailErrorStrategy { private final MultiSourceReader multiSourceReader; + private final ParserEnvironment environment; - public ExtendedBailStrategy(MultiSourceReader multiSourceReader) { + public ExtendedBailStrategy(MultiSourceReader multiSourceReader, ParserEnvironment environment) { this.multiSourceReader = multiSourceReader; + this.environment = environment; } @Override @@ -37,23 +43,38 @@ public Token recoverInline(Parser recognizer) throws RecognitionException { InvalidSyntaxException mkMoreTokensException(Token token) { SourceLocation sourceLocation = AntlrHelper.createSourceLocation(multiSourceReader, token); String sourcePreview = AntlrHelper.createPreview(multiSourceReader, token.getLine()); - return new InvalidSyntaxException(sourceLocation, - "There are more tokens in the query that have not been consumed", - sourcePreview, token.getText(), null); + return new MoreTokensSyntaxException(environment.getI18N(), sourceLocation, + token.getText(), sourcePreview); } private InvalidSyntaxException mkException(Parser recognizer, RecognitionException cause) { - String sourcePreview = null; - String offendingToken = null; - SourceLocation sourceLocation = null; + String sourcePreview; + String offendingToken; + final SourceLocation sourceLocation; Token currentToken = recognizer.getCurrentToken(); if (currentToken != null) { sourceLocation = AntlrHelper.createSourceLocation(multiSourceReader, currentToken); offendingToken = currentToken.getText(); sourcePreview = AntlrHelper.createPreview(multiSourceReader, currentToken.getLine()); + } else { + sourcePreview = null; + offendingToken = null; + sourceLocation = null; + } + + String msgKey; + List args; + SourceLocation location = sourceLocation == null ? SourceLocation.EMPTY : sourceLocation; + if (offendingToken == null) { + msgKey = "InvalidSyntaxBail.noToken"; + args = ImmutableList.of(location.getLine(), location.getColumn()); + } else { + msgKey = "InvalidSyntaxBail.full"; + args = ImmutableList.of(offendingToken, sourceLocation.getLine(), sourceLocation.getColumn()); } - return new InvalidSyntaxException(sourceLocation, null, sourcePreview, offendingToken, cause); + String msg = environment.getI18N().msg(msgKey, args); + return new InvalidSyntaxException(msg, sourceLocation, offendingToken, sourcePreview, cause); } } diff --git a/src/main/java/graphql/parser/GraphqlAntlrToLanguage.java b/src/main/java/graphql/parser/GraphqlAntlrToLanguage.java index da0209c1f6..eb5070bc66 100644 --- a/src/main/java/graphql/parser/GraphqlAntlrToLanguage.java +++ b/src/main/java/graphql/parser/GraphqlAntlrToLanguage.java @@ -5,6 +5,7 @@ import graphql.Assert; import graphql.Internal; import graphql.collect.ImmutableKit; +import graphql.i18n.I18n; import graphql.language.Argument; import graphql.language.ArrayValue; import graphql.language.BooleanValue; @@ -34,6 +35,7 @@ import graphql.language.InterfaceTypeDefinition; import graphql.language.InterfaceTypeExtensionDefinition; import graphql.language.ListType; +import graphql.language.Node; import graphql.language.NodeBuilder; import graphql.language.NonNullType; import graphql.language.NullValue; @@ -67,10 +69,12 @@ import org.antlr.v4.runtime.Token; import org.antlr.v4.runtime.tree.TerminalNode; +import javax.annotation.Nullable; import java.math.BigDecimal; import java.math.BigInteger; import java.util.ArrayList; import java.util.List; +import java.util.Map; import static graphql.Assert.assertShouldNeverHappen; import static graphql.collect.ImmutableKit.emptyList; @@ -88,16 +92,16 @@ public class GraphqlAntlrToLanguage { private final CommonTokenStream tokens; private final MultiSourceReader multiSourceReader; private final ParserOptions parserOptions; + private final Map, ParserRuleContext> nodeToRuleMap; + private final I18n i18N; - - public GraphqlAntlrToLanguage(CommonTokenStream tokens, MultiSourceReader multiSourceReader) { - this(tokens, multiSourceReader, null); - } - - public GraphqlAntlrToLanguage(CommonTokenStream tokens, MultiSourceReader multiSourceReader, ParserOptions parserOptions) { + public GraphqlAntlrToLanguage(CommonTokenStream tokens, MultiSourceReader multiSourceReader, ParserOptions parserOptions, I18n i18N, @Nullable Map, ParserRuleContext> nodeToRuleMap) { this.tokens = tokens; this.multiSourceReader = multiSourceReader; this.parserOptions = ofNullable(parserOptions).orElse(ParserOptions.getDefaultParserOptions()); + this.i18N = i18N; + this.nodeToRuleMap = nodeToRuleMap; + } public ParserOptions getParserOptions() { @@ -111,7 +115,7 @@ public Document createDocument(GraphqlParser.DocumentContext ctx) { Document.Builder document = Document.newDocument(); addCommonData(document, ctx); document.definitions(map(ctx.definition(), this::createDefinition)); - return document.build(); + return captureRuleContext(document.build(), ctx); } protected Definition createDefinition(GraphqlParser.DefinitionContext definitionContext) { @@ -142,7 +146,7 @@ protected OperationDefinition createOperationDefinition(GraphqlParser.OperationD operationDefinition.variableDefinitions(createVariableDefinitions(ctx.variableDefinitions())); operationDefinition.selectionSet(createSelectionSet(ctx.selectionSet())); operationDefinition.directives(createDirectives(ctx.directives())); - return operationDefinition.build(); + return captureRuleContext(operationDefinition.build(), ctx); } protected OperationDefinition.Operation parseOperation(GraphqlParser.OperationTypeContext operationTypeContext) { @@ -162,7 +166,7 @@ protected FragmentSpread createFragmentSpread(GraphqlParser.FragmentSpreadContex FragmentSpread.Builder fragmentSpread = FragmentSpread.newFragmentSpread().name(ctx.fragmentName().getText()); addCommonData(fragmentSpread, ctx); fragmentSpread.directives(createDirectives(ctx.directives())); - return fragmentSpread.build(); + return captureRuleContext(fragmentSpread.build(), ctx); } protected List createVariableDefinitions(GraphqlParser.VariableDefinitionsContext ctx) { @@ -182,7 +186,7 @@ protected VariableDefinition createVariableDefinition(GraphqlParser.VariableDefi } variableDefinition.type(createType(ctx.type())); variableDefinition.directives(createDirectives(ctx.directives())); - return variableDefinition.build(); + return captureRuleContext(variableDefinition.build(), ctx); } @@ -193,7 +197,7 @@ protected FragmentDefinition createFragmentDefinition(GraphqlParser.FragmentDefi fragmentDefinition.typeCondition(TypeName.newTypeName().name(ctx.typeCondition().typeName().getText()).build()); fragmentDefinition.directives(createDirectives(ctx.directives())); fragmentDefinition.selectionSet(createSelectionSet(ctx.selectionSet())); - return fragmentDefinition.build(); + return captureRuleContext(fragmentDefinition.build(), ctx); } @@ -213,11 +217,11 @@ protected SelectionSet createSelectionSet(GraphqlParser.SelectionSetContext ctx) if (selectionContext.inlineFragment() != null) { return createInlineFragment(selectionContext.inlineFragment()); } - return (Selection) Assert.assertShouldNeverHappen(); + return Assert.assertShouldNeverHappen(); }); builder.selections(selections); - return builder.build(); + return captureRuleContext(builder.build(), ctx); } @@ -232,7 +236,7 @@ protected Field createField(GraphqlParser.FieldContext ctx) { builder.directives(createDirectives(ctx.directives())); builder.arguments(createArguments(ctx.arguments())); builder.selectionSet(createSelectionSet(ctx.selectionSet())); - return builder.build(); + return captureRuleContext(builder.build(), ctx); } @@ -244,7 +248,7 @@ protected InlineFragment createInlineFragment(GraphqlParser.InlineFragmentContex } inlineFragment.directives(createDirectives(ctx.directives())); inlineFragment.selectionSet(createSelectionSet(ctx.selectionSet())); - return inlineFragment.build(); + return captureRuleContext(inlineFragment.build(), ctx); } //MARKER END: Here GraphqlOperation.g4 specific methods end @@ -335,7 +339,7 @@ protected TypeName createTypeName(GraphqlParser.TypeNameContext ctx) { TypeName.Builder builder = TypeName.newTypeName(); builder.name(ctx.name().getText()); addCommonData(builder, ctx); - return builder.build(); + return captureRuleContext(builder.build(), ctx); } protected NonNullType createNonNullType(GraphqlParser.NonNullTypeContext ctx) { @@ -348,14 +352,14 @@ protected NonNullType createNonNullType(GraphqlParser.NonNullTypeContext ctx) { } else { return assertShouldNeverHappen(); } - return builder.build(); + return captureRuleContext(builder.build(), ctx); } protected ListType createListType(GraphqlParser.ListTypeContext ctx) { ListType.Builder builder = ListType.newListType(); addCommonData(builder, ctx); builder.type(createType(ctx.type())); - return builder.build(); + return captureRuleContext(builder.build(), ctx); } protected Argument createArgument(GraphqlParser.ArgumentContext ctx) { @@ -363,7 +367,7 @@ protected Argument createArgument(GraphqlParser.ArgumentContext ctx) { addCommonData(builder, ctx); builder.name(ctx.name().getText()); builder.value(createValue(ctx.valueWithVariable())); - return builder.build(); + return captureRuleContext(builder.build(), ctx); } protected List createArguments(GraphqlParser.ArgumentsContext ctx) { @@ -386,7 +390,7 @@ protected Directive createDirective(GraphqlParser.DirectiveContext ctx) { builder.name(ctx.name().getText()); addCommonData(builder, ctx); builder.arguments(createArguments(ctx.arguments())); - return builder.build(); + return captureRuleContext(builder.build(), ctx); } protected SchemaDefinition createSchemaDefinition(GraphqlParser.SchemaDefinitionContext ctx) { @@ -395,7 +399,7 @@ protected SchemaDefinition createSchemaDefinition(GraphqlParser.SchemaDefinition def.directives(createDirectives(ctx.directives())); def.description(newDescription(ctx.description())); def.operationTypeDefinitions(map(ctx.operationTypeDefinition(), this::createOperationTypeDefinition)); - return def.build(); + return captureRuleContext(def.build(), ctx); } private SDLDefinition creationSchemaExtension(GraphqlParser.SchemaExtensionContext ctx) { @@ -411,7 +415,7 @@ private SDLDefinition creationSchemaExtension(GraphqlParser.SchemaExtensionConte List operationTypeDefs = map(ctx.operationTypeDefinition(), this::createOperationTypeDefinition); def.operationTypeDefinitions(operationTypeDefs); - return def.build(); + return captureRuleContext(def.build(), ctx); } @@ -420,7 +424,7 @@ protected OperationTypeDefinition createOperationTypeDefinition(GraphqlParser.Op def.name(ctx.operationType().getText()); def.typeName(createTypeName(ctx.typeName())); addCommonData(def, ctx); - return def.build(); + return captureRuleContext(def.build(), ctx); } protected ScalarTypeDefinition createScalarTypeDefinition(GraphqlParser.ScalarTypeDefinitionContext ctx) { @@ -429,7 +433,7 @@ protected ScalarTypeDefinition createScalarTypeDefinition(GraphqlParser.ScalarTy addCommonData(def, ctx); def.description(newDescription(ctx.description())); def.directives(createDirectives(ctx.directives())); - return def.build(); + return captureRuleContext(def.build(), ctx); } protected ScalarTypeExtensionDefinition createScalarTypeExtensionDefinition(GraphqlParser.ScalarTypeExtensionDefinitionContext ctx) { @@ -437,7 +441,7 @@ protected ScalarTypeExtensionDefinition createScalarTypeExtensionDefinition(Grap def.name(ctx.name().getText()); addCommonData(def, ctx); def.directives(createDirectives(ctx.directives())); - return def.build(); + return captureRuleContext(def.build(), ctx); } protected ObjectTypeDefinition createObjectTypeDefinition(GraphqlParser.ObjectTypeDefinitionContext ctx) { @@ -452,7 +456,7 @@ protected ObjectTypeDefinition createObjectTypeDefinition(GraphqlParser.ObjectTy if (ctx.fieldsDefinition() != null) { def.fieldDefinitions(createFieldDefinitions(ctx.fieldsDefinition())); } - return def.build(); + return captureRuleContext(def.build(), ctx); } protected ObjectTypeExtensionDefinition createObjectTypeExtensionDefinition(GraphqlParser.ObjectTypeExtensionDefinitionContext ctx) { @@ -466,7 +470,7 @@ protected ObjectTypeExtensionDefinition createObjectTypeExtensionDefinition(Grap if (ctx.extensionFieldsDefinition() != null) { def.fieldDefinitions(createFieldDefinitions(ctx.extensionFieldsDefinition())); } - return def.build(); + return captureRuleContext(def.build(), ctx); } protected List createFieldDefinitions(GraphqlParser.FieldsDefinitionContext ctx) { @@ -494,7 +498,7 @@ protected FieldDefinition createFieldDefinition(GraphqlParser.FieldDefinitionCon if (ctx.argumentsDefinition() != null) { def.inputValueDefinitions(createInputValueDefinitions(ctx.argumentsDefinition().inputValueDefinition())); } - return def.build(); + return captureRuleContext(def.build(), ctx); } protected List createInputValueDefinitions(List defs) { @@ -511,7 +515,7 @@ protected InputValueDefinition createInputValueDefinition(GraphqlParser.InputVal def.defaultValue(createValue(ctx.defaultValue().value())); } def.directives(createDirectives(ctx.directives())); - return def.build(); + return captureRuleContext(def.build(), ctx); } protected InterfaceTypeDefinition createInterfaceTypeDefinition(GraphqlParser.InterfaceTypeDefinitionContext ctx) { @@ -524,7 +528,7 @@ protected InterfaceTypeDefinition createInterfaceTypeDefinition(GraphqlParser.In List implementz = getImplementz(implementsInterfacesContext); def.implementz(implementz); def.definitions(createFieldDefinitions(ctx.fieldsDefinition())); - return def.build(); + return captureRuleContext(def.build(), ctx); } protected InterfaceTypeExtensionDefinition createInterfaceTypeExtensionDefinition(GraphqlParser.InterfaceTypeExtensionDefinitionContext ctx) { @@ -536,7 +540,7 @@ protected InterfaceTypeExtensionDefinition createInterfaceTypeExtensionDefinitio List implementz = getImplementz(implementsInterfacesContext); def.implementz(implementz); def.definitions(createFieldDefinitions(ctx.extensionFieldsDefinition())); - return def.build(); + return captureRuleContext(def.build(), ctx); } protected UnionTypeDefinition createUnionTypeDefinition(GraphqlParser.UnionTypeDefinitionContext ctx) { @@ -555,7 +559,7 @@ protected UnionTypeDefinition createUnionTypeDefinition(GraphqlParser.UnionTypeD } } def.memberTypes(members); - return def.build(); + return captureRuleContext(def.build(), ctx); } protected UnionTypeExtensionDefinition createUnionTypeExtensionDefinition(GraphqlParser.UnionTypeExtensionDefinitionContext ctx) { @@ -572,7 +576,7 @@ protected UnionTypeExtensionDefinition createUnionTypeExtensionDefinition(Graphq } def.memberTypes(members); } - return def.build(); + return captureRuleContext(def.build(), ctx); } protected EnumTypeDefinition createEnumTypeDefinition(GraphqlParser.EnumTypeDefinitionContext ctx) { @@ -585,7 +589,7 @@ protected EnumTypeDefinition createEnumTypeDefinition(GraphqlParser.EnumTypeDefi def.enumValueDefinitions( map(ctx.enumValueDefinitions().enumValueDefinition(), this::createEnumValueDefinition)); } - return def.build(); + return captureRuleContext(def.build(), ctx); } protected EnumTypeExtensionDefinition createEnumTypeExtensionDefinition(GraphqlParser.EnumTypeExtensionDefinitionContext ctx) { @@ -597,7 +601,7 @@ protected EnumTypeExtensionDefinition createEnumTypeExtensionDefinition(GraphqlP def.enumValueDefinitions( map(ctx.extensionEnumValueDefinitions().enumValueDefinition(), this::createEnumValueDefinition)); } - return def.build(); + return captureRuleContext(def.build(), ctx); } protected EnumValueDefinition createEnumValueDefinition(GraphqlParser.EnumValueDefinitionContext ctx) { @@ -606,7 +610,7 @@ protected EnumValueDefinition createEnumValueDefinition(GraphqlParser.EnumValueD addCommonData(def, ctx); def.description(newDescription(ctx.description())); def.directives(createDirectives(ctx.directives())); - return def.build(); + return captureRuleContext(def.build(), ctx); } protected InputObjectTypeDefinition createInputObjectTypeDefinition(GraphqlParser.InputObjectTypeDefinitionContext ctx) { @@ -618,7 +622,7 @@ protected InputObjectTypeDefinition createInputObjectTypeDefinition(GraphqlParse if (ctx.inputObjectValueDefinitions() != null) { def.inputValueDefinitions(createInputValueDefinitions(ctx.inputObjectValueDefinitions().inputValueDefinition())); } - return def.build(); + return captureRuleContext(def.build(), ctx); } protected InputObjectTypeExtensionDefinition createInputObjectTypeExtensionDefinition(GraphqlParser.InputObjectTypeExtensionDefinitionContext ctx) { @@ -629,7 +633,7 @@ protected InputObjectTypeExtensionDefinition createInputObjectTypeExtensionDefin if (ctx.extensionInputObjectValueDefinitions() != null) { def.inputValueDefinitions(createInputValueDefinitions(ctx.extensionInputObjectValueDefinitions().inputValueDefinition())); } - return def.build(); + return captureRuleContext(def.build(), ctx); } protected DirectiveDefinition createDirectiveDefinition(GraphqlParser.DirectiveDefinitionContext ctx) { @@ -650,41 +654,41 @@ protected DirectiveDefinition createDirectiveDefinition(GraphqlParser.DirectiveD if (ctx.argumentsDefinition() != null) { def.inputValueDefinitions(createInputValueDefinitions(ctx.argumentsDefinition().inputValueDefinition())); } - return def.build(); + return captureRuleContext(def.build(), ctx); } protected DirectiveLocation createDirectiveLocation(GraphqlParser.DirectiveLocationContext ctx) { DirectiveLocation.Builder def = DirectiveLocation.newDirectiveLocation(); def.name(ctx.name().getText()); addCommonData(def, ctx); - return def.build(); + return captureRuleContext(def.build(), ctx); } protected Value createValue(GraphqlParser.ValueWithVariableContext ctx) { if (ctx.IntValue() != null) { IntValue.Builder intValue = IntValue.newIntValue().value(new BigInteger(ctx.IntValue().getText())); addCommonData(intValue, ctx); - return intValue.build(); + return captureRuleContext(intValue.build(), ctx); } else if (ctx.FloatValue() != null) { FloatValue.Builder floatValue = FloatValue.newFloatValue().value(new BigDecimal(ctx.FloatValue().getText())); addCommonData(floatValue, ctx); - return floatValue.build(); + return captureRuleContext(floatValue.build(), ctx); } else if (ctx.BooleanValue() != null) { BooleanValue.Builder booleanValue = BooleanValue.newBooleanValue().value(Boolean.parseBoolean(ctx.BooleanValue().getText())); addCommonData(booleanValue, ctx); - return booleanValue.build(); + return captureRuleContext(booleanValue.build(), ctx); } else if (ctx.NullValue() != null) { NullValue.Builder nullValue = NullValue.newNullValue(); addCommonData(nullValue, ctx); - return nullValue.build(); + return captureRuleContext(nullValue.build(), ctx); } else if (ctx.StringValue() != null) { StringValue.Builder stringValue = StringValue.newStringValue().value(quotedString(ctx.StringValue())); addCommonData(stringValue, ctx); - return stringValue.build(); + return captureRuleContext(stringValue.build(), ctx); } else if (ctx.enumValue() != null) { EnumValue.Builder enumValue = EnumValue.newEnumValue().name(ctx.enumValue().getText()); addCommonData(enumValue, ctx); - return enumValue.build(); + return captureRuleContext(enumValue.build(), ctx); } else if (ctx.arrayValueWithVariable() != null) { ArrayValue.Builder arrayValue = ArrayValue.newArrayValue(); addCommonData(arrayValue, ctx); @@ -692,7 +696,7 @@ protected Value createValue(GraphqlParser.ValueWithVariableContext ctx) { for (GraphqlParser.ValueWithVariableContext valueWithVariableContext : ctx.arrayValueWithVariable().valueWithVariable()) { values.add(createValue(valueWithVariableContext)); } - return arrayValue.values(values).build(); + return captureRuleContext(arrayValue.values(values).build(), ctx); } else if (ctx.objectValueWithVariable() != null) { ObjectValue.Builder objectValue = ObjectValue.newObjectValue(); addCommonData(objectValue, ctx); @@ -706,11 +710,11 @@ protected Value createValue(GraphqlParser.ValueWithVariableContext ctx) { .build(); objectFields.add(objectField); } - return objectValue.objectFields(objectFields).build(); + return captureRuleContext(objectValue.objectFields(objectFields).build(), ctx); } else if (ctx.variable() != null) { VariableReference.Builder variableReference = VariableReference.newVariableReference().name(ctx.variable().name().getText()); addCommonData(variableReference, ctx); - return variableReference.build(); + return captureRuleContext(variableReference.build(), ctx); } return assertShouldNeverHappen(); } @@ -719,27 +723,27 @@ protected Value createValue(GraphqlParser.ValueContext ctx) { if (ctx.IntValue() != null) { IntValue.Builder intValue = IntValue.newIntValue().value(new BigInteger(ctx.IntValue().getText())); addCommonData(intValue, ctx); - return intValue.build(); + return captureRuleContext(intValue.build(), ctx); } else if (ctx.FloatValue() != null) { FloatValue.Builder floatValue = FloatValue.newFloatValue().value(new BigDecimal(ctx.FloatValue().getText())); addCommonData(floatValue, ctx); - return floatValue.build(); + return captureRuleContext(floatValue.build(), ctx); } else if (ctx.BooleanValue() != null) { BooleanValue.Builder booleanValue = BooleanValue.newBooleanValue().value(Boolean.parseBoolean(ctx.BooleanValue().getText())); addCommonData(booleanValue, ctx); - return booleanValue.build(); + return captureRuleContext(booleanValue.build(), ctx); } else if (ctx.NullValue() != null) { NullValue.Builder nullValue = NullValue.newNullValue(); addCommonData(nullValue, ctx); - return nullValue.build(); + return captureRuleContext(nullValue.build(), ctx); } else if (ctx.StringValue() != null) { StringValue.Builder stringValue = StringValue.newStringValue().value(quotedString(ctx.StringValue())); addCommonData(stringValue, ctx); - return stringValue.build(); + return captureRuleContext(stringValue.build(), ctx); } else if (ctx.enumValue() != null) { EnumValue.Builder enumValue = EnumValue.newEnumValue().name(ctx.enumValue().getText()); addCommonData(enumValue, ctx); - return enumValue.build(); + return captureRuleContext(enumValue.build(), ctx); } else if (ctx.arrayValue() != null) { ArrayValue.Builder arrayValue = ArrayValue.newArrayValue(); addCommonData(arrayValue, ctx); @@ -747,7 +751,7 @@ protected Value createValue(GraphqlParser.ValueContext ctx) { for (GraphqlParser.ValueContext valueContext : ctx.arrayValue().value()) { values.add(createValue(valueContext)); } - return arrayValue.values(values).build(); + return captureRuleContext(arrayValue.values(values).build(), ctx); } else if (ctx.objectValue() != null) { ObjectValue.Builder objectValue = ObjectValue.newObjectValue(); addCommonData(objectValue, ctx); @@ -760,7 +764,7 @@ protected Value createValue(GraphqlParser.ValueContext ctx) { .build(); objectFields.add(objectField); } - return objectValue.objectFields(objectFields).build(); + return captureRuleContext(objectValue.objectFields(objectFields).build(), ctx); } return assertShouldNeverHappen(); } @@ -772,7 +776,7 @@ protected String quotedString(TerminalNode terminalNode) { if (multiLine) { return parseTripleQuotedString(strText); } else { - return parseSingleQuotedString(strText, sourceLocation); + return parseSingleQuotedString(i18N, strText, sourceLocation); } } @@ -804,7 +808,7 @@ private void addIgnoredChars(ParserRuleContext ctx, NodeBuilder nodeBuilder) { private List mapTokenToIgnoredChar(List tokens) { if (tokens == null) { - return ImmutableKit.emptyList(); + return emptyList(); } return map(tokens, this::createIgnoredChar); @@ -849,7 +853,7 @@ protected Description newDescription(GraphqlParser.DescriptionContext descriptio if (multiLine) { content = parseTripleQuotedString(content); } else { - content = parseSingleQuotedString(content, sourceLocation); + content = parseSingleQuotedString(i18N, content, sourceLocation); } return new Description(content, sourceLocation, multiLine); } @@ -918,4 +922,11 @@ private List getImplementz(GraphqlParser.ImplementsInterfacesContext imple } return implementz; } + + private > T captureRuleContext(T node, ParserRuleContext ctx) { + if (nodeToRuleMap != null) { + nodeToRuleMap.put(node, ctx); + } + return node; + } } diff --git a/src/main/java/graphql/parser/InvalidSyntaxException.java b/src/main/java/graphql/parser/InvalidSyntaxException.java index 9136512c6f..939dfd2e2d 100644 --- a/src/main/java/graphql/parser/InvalidSyntaxException.java +++ b/src/main/java/graphql/parser/InvalidSyntaxException.java @@ -2,6 +2,7 @@ import graphql.GraphQLException; +import graphql.Internal; import graphql.InvalidSyntaxError; import graphql.PublicApi; import graphql.language.SourceLocation; @@ -20,35 +21,20 @@ public class InvalidSyntaxException extends GraphQLException { private final String offendingToken; private final SourceLocation location; - InvalidSyntaxException(SourceLocation location, String msg, String sourcePreview, String offendingToken, Exception cause) { + @Internal + protected InvalidSyntaxException(String msg, SourceLocation location, String offendingToken, String sourcePreview, Exception cause) { super(cause); - this.message = mkMessage(msg, offendingToken, location); + this.message = msg; this.sourcePreview = sourcePreview; this.offendingToken = offendingToken; this.location = location; } - private String mkMessage(String msg, String offendingToken, SourceLocation location) { - StringBuilder sb = new StringBuilder(); - sb.append("Invalid Syntax :"); - if (msg != null) { - sb.append(" ").append(msg); - } - if (offendingToken != null) { - sb.append(String.format(" offending token '%s'", offendingToken)); - } - if (location != null) { - sb.append(String.format(" at line %d column %d", location.getLine(), location.getColumn())); - } - return sb.toString(); - } - public InvalidSyntaxError toInvalidSyntaxError() { List sourceLocations = location == null ? null : Collections.singletonList(location); return new InvalidSyntaxError(sourceLocations, message, sourcePreview, offendingToken); } - @Override public String getMessage() { return message; diff --git a/src/main/java/graphql/parser/NodeToRuleCapturingParser.java b/src/main/java/graphql/parser/NodeToRuleCapturingParser.java new file mode 100644 index 0000000000..f63fe58166 --- /dev/null +++ b/src/main/java/graphql/parser/NodeToRuleCapturingParser.java @@ -0,0 +1,49 @@ +package graphql.parser; + +import graphql.Internal; +import graphql.language.Node; +import org.antlr.v4.runtime.CommonTokenStream; +import org.antlr.v4.runtime.ParserRuleContext; + +import java.util.HashMap; +import java.util.Map; + +/** + * A parser that will capture parsing context data which can be later used for accessing tokens that are discarded + * during the conventional parsing process (like comments). + */ +@Internal +public class NodeToRuleCapturingParser extends Parser { + private final ParserContext parserContext; + + public NodeToRuleCapturingParser() { + parserContext = new ParserContext(); + } + + @Override + protected GraphqlAntlrToLanguage getAntlrToLanguage(CommonTokenStream tokens, MultiSourceReader multiSourceReader, ParserEnvironment environment) { + parserContext.tokens = tokens; + return new GraphqlAntlrToLanguage(tokens, multiSourceReader, environment.getParserOptions(), environment.getI18N(), parserContext.nodeToRuleMap); + } + + public ParserContext getParserContext() { + return parserContext; + } + + static public class ParserContext { + private final Map, ParserRuleContext> nodeToRuleMap; + private CommonTokenStream tokens; + + public ParserContext() { + this.nodeToRuleMap = new HashMap<>(); + } + + protected CommonTokenStream getTokens() { + return tokens; + } + + protected Map, ParserRuleContext> getNodeToRuleMap() { + return nodeToRuleMap; + } + } +} diff --git a/src/main/java/graphql/parser/ParseCancelledException.java b/src/main/java/graphql/parser/ParseCancelledException.java deleted file mode 100644 index c416c12504..0000000000 --- a/src/main/java/graphql/parser/ParseCancelledException.java +++ /dev/null @@ -1,12 +0,0 @@ -package graphql.parser; - -import graphql.PublicApi; -import graphql.language.SourceLocation; - -@PublicApi -public class ParseCancelledException extends InvalidSyntaxException { - - public ParseCancelledException(String msg, SourceLocation sourceLocation, String offendingToken) { - super(sourceLocation, msg, null, offendingToken, null); - } -} diff --git a/src/main/java/graphql/parser/Parser.java b/src/main/java/graphql/parser/Parser.java index d2726dda68..c1aac322f3 100644 --- a/src/main/java/graphql/parser/Parser.java +++ b/src/main/java/graphql/parser/Parser.java @@ -1,5 +1,7 @@ package graphql.parser; +import com.google.common.collect.ImmutableList; +import graphql.DeprecatedAt; import graphql.Internal; import graphql.PublicApi; import graphql.language.Document; @@ -10,6 +12,7 @@ import graphql.parser.antlr.GraphqlBaseListener; import graphql.parser.antlr.GraphqlLexer; import graphql.parser.antlr.GraphqlParser; +import graphql.parser.exceptions.ParseCancelledException; import org.antlr.v4.runtime.BaseErrorListener; import org.antlr.v4.runtime.CharStreams; import org.antlr.v4.runtime.CodePointCharStream; @@ -36,10 +39,10 @@ *

* You should not generally need to call this class as the {@link graphql.GraphQL} code sets this up for you * but if you are doing specific graphql utilities this class is essential. - * + *

* Graphql syntax has a series of characters, such as spaces, new lines and commas that are not considered relevant * to the syntax. However they can be captured and associated with the AST elements they belong to. - * + *

* This costs more memory but for certain use cases (like editors) this maybe be useful. We have chosen to no capture * ignored characters by default but you can turn this on, either per parse or statically for the whole JVM * via {@link ParserOptions#setDefaultParserOptions(ParserOptions)} ()}} @@ -54,6 +57,19 @@ public class Parser { @Internal public static final int CHANNEL_WHITESPACE = 3; + /** + * Parses a string input into a graphql AST {@link Document} + * + * @param environment the parser environment to use + * + * @return an AST {@link Document} + * + * @throws InvalidSyntaxException if the document is not valid graphql syntax + */ + public static Document parse(ParserEnvironment environment) throws InvalidSyntaxException { + return new Parser().parseDocument(environment); + } + /** * Parses a string input into a graphql AST {@link Document} * @@ -67,6 +83,7 @@ public static Document parse(String input) throws InvalidSyntaxException { return new Parser().parseDocument(input); } + /** * Parses a string input into a graphql AST {@link Value} * @@ -93,6 +110,19 @@ public static Type parseType(String input) throws InvalidSyntaxException { return new Parser().parseTypeImpl(input); } + /** + * Parses document text into a graphql AST {@link Document} + * + * @param environment the parser environment to sue + * + * @return an AST {@link Document} + * + * @throws InvalidSyntaxException if the input is not valid graphql syntax + */ + public Document parseDocument(ParserEnvironment environment) throws InvalidSyntaxException { + return parseDocumentImpl(environment); + } + /** * Parses a string input into a graphql AST {@link Document} * @@ -106,6 +136,22 @@ public Document parseDocument(String input) throws InvalidSyntaxException { return parseDocument(input, (ParserOptions) null); } + /** + * Parses reader input into a graphql AST {@link Document} + * + * @param reader the reader input to parse + * + * @return an AST {@link Document} + * + * @throws InvalidSyntaxException if the input is not valid graphql syntax + */ + public Document parseDocument(Reader reader) throws InvalidSyntaxException { + ParserEnvironment parserEnvironment = ParserEnvironment.newParserEnvironment() + .document(reader) + .build(); + return parseDocumentImpl(parserEnvironment); + } + /** * Parses a string input into a graphql AST {@link Document} * @@ -115,7 +161,10 @@ public Document parseDocument(String input) throws InvalidSyntaxException { * @return an AST {@link Document} * * @throws InvalidSyntaxException if the input is not valid graphql syntax + * @deprecated use {#{@link #parse(ParserEnvironment)}} instead */ + @DeprecatedAt("2022-08-31") + @Deprecated public Document parseDocument(String input, String sourceName) throws InvalidSyntaxException { MultiSourceReader multiSourceReader = MultiSourceReader.newMultiSourceReader() .string(input, sourceName) @@ -133,7 +182,10 @@ public Document parseDocument(String input, String sourceName) throws InvalidSyn * @return an AST {@link Document} * * @throws InvalidSyntaxException if the input is not valid graphql syntax + * @deprecated use {#{@link #parse(ParserEnvironment)}} instead */ + @DeprecatedAt("2022-08-31") + @Deprecated public Document parseDocument(String input, ParserOptions parserOptions) throws InvalidSyntaxException { MultiSourceReader multiSourceReader = MultiSourceReader.newMultiSourceReader() .string(input, null) @@ -142,19 +194,6 @@ public Document parseDocument(String input, ParserOptions parserOptions) throws return parseDocument(multiSourceReader, parserOptions); } - /** - * Parses reader input into a graphql AST {@link Document} - * - * @param reader the reader input to parse - * - * @return an AST {@link Document} - * - * @throws InvalidSyntaxException if the input is not valid graphql syntax - */ - public Document parseDocument(Reader reader) throws InvalidSyntaxException { - return parseDocumentImpl(reader, null); - } - /** * Parses reader input into a graphql AST {@link Document} * @@ -164,18 +203,25 @@ public Document parseDocument(Reader reader) throws InvalidSyntaxException { * @return an AST {@link Document} * * @throws InvalidSyntaxException if the input is not valid graphql syntax + * @deprecated use {#{@link #parse(ParserEnvironment)}} instead */ + @DeprecatedAt("2022-08-31") + @Deprecated public Document parseDocument(Reader reader, ParserOptions parserOptions) throws InvalidSyntaxException { - return parseDocumentImpl(reader, parserOptions); + ParserEnvironment parserEnvironment = ParserEnvironment.newParserEnvironment() + .document(reader) + .parserOptions(parserOptions) + .build(); + return parseDocumentImpl(parserEnvironment); } - private Document parseDocumentImpl(Reader reader, ParserOptions parserOptions) throws InvalidSyntaxException { + private Document parseDocumentImpl(ParserEnvironment environment) throws InvalidSyntaxException { BiFunction nodeFunction = (parser, toLanguage) -> { GraphqlParser.DocumentContext documentContext = parser.document(); Document doc = toLanguage.createDocument(documentContext); return new Object[]{documentContext, doc}; }; - return (Document) parseImpl(reader, nodeFunction, parserOptions); + return (Document) parseImpl(environment, nodeFunction); } private Value parseValueImpl(String input) throws InvalidSyntaxException { @@ -188,7 +234,8 @@ private Value parseValueImpl(String input) throws InvalidSyntaxException { .string(input, null) .trackData(true) .build(); - return (Value) parseImpl(multiSourceReader, nodeFunction, null); + ParserEnvironment parserEnvironment = ParserEnvironment.newParserEnvironment().document(multiSourceReader).build(); + return (Value) parseImpl(parserEnvironment, nodeFunction); } private Type parseTypeImpl(String input) throws InvalidSyntaxException { @@ -201,11 +248,14 @@ private Type parseTypeImpl(String input) throws InvalidSyntaxException { .string(input, null) .trackData(true) .build(); - return (Type) parseImpl(multiSourceReader, nodeFunction, null); + + ParserEnvironment parserEnvironment = ParserEnvironment.newParserEnvironment().document(multiSourceReader).build(); + return (Type) parseImpl(parserEnvironment, nodeFunction); } - private Node parseImpl(Reader reader, BiFunction nodeFunction, ParserOptions parserOptions) throws InvalidSyntaxException { + private Node parseImpl(ParserEnvironment environment, BiFunction nodeFunction) throws InvalidSyntaxException { MultiSourceReader multiSourceReader; + Reader reader = environment.getDocument(); if (reader instanceof MultiSourceReader) { multiSourceReader = (MultiSourceReader) reader; } else { @@ -223,20 +273,31 @@ private Node parseImpl(Reader reader, BiFunction recognizer, Object offendingSymbol, int line, int charPositionInLine, String msg, RecognitionException e) { + public void syntaxError(Recognizer recognizer, Object offendingSymbol, int line, int charPositionInLine, String antlerMsg, RecognitionException e) { SourceLocation sourceLocation = AntlrHelper.createSourceLocation(multiSourceReader, line, charPositionInLine); String preview = AntlrHelper.createPreview(multiSourceReader, line); - throw new InvalidSyntaxException(sourceLocation, msg, preview, null, null); + String msgKey; + List args; + if (antlerMsg == null) { + msgKey = "InvalidSyntax.noMessage"; + args = ImmutableList.of(sourceLocation.getLine(), sourceLocation.getColumn()); + } else { + msgKey = "InvalidSyntax.full"; + args = ImmutableList.of(antlerMsg, sourceLocation.getLine(), sourceLocation.getColumn()); + } + String msg = environment.getI18N().msg(msgKey, args); + throw new InvalidSyntaxException(msg, sourceLocation, null, preview, null); } }); // default in the parser options if they are not set + ParserOptions parserOptions = environment.getParserOptions(); parserOptions = Optional.ofNullable(parserOptions).orElse(ParserOptions.getDefaultParserOptions()); // this lexer wrapper allows us to stop lexing when too many tokens are in place. This prevents DOS attacks. int maxTokens = parserOptions.getMaxTokens(); int maxWhitespaceTokens = parserOptions.getMaxWhitespaceTokens(); - BiConsumer onTooManyTokens = (maxTokenCount, token) -> throwCancelParseIfTooManyTokens(token, maxTokenCount, multiSourceReader); + BiConsumer onTooManyTokens = (maxTokenCount, token) -> throwCancelParseIfTooManyTokens(environment, token, maxTokenCount, multiSourceReader); SafeTokenSource safeTokenSource = new SafeTokenSource(lexer, maxTokens, maxWhitespaceTokens, onTooManyTokens); CommonTokenStream tokens = new CommonTokenStream(safeTokenSource); @@ -245,16 +306,13 @@ public void syntaxError(Recognizer recognizer, Object offendingSymbol, int parser.removeErrorListeners(); parser.getInterpreter().setPredictionMode(PredictionMode.SLL); - ExtendedBailStrategy bailStrategy = new ExtendedBailStrategy(multiSourceReader); + ExtendedBailStrategy bailStrategy = new ExtendedBailStrategy(multiSourceReader, environment); parser.setErrorHandler(bailStrategy); // preserve old protected call semantics - remove at some point - GraphqlAntlrToLanguage toLanguage = getAntlrToLanguage(tokens, multiSourceReader); - if (toLanguage == null) { - toLanguage = getAntlrToLanguage(tokens, multiSourceReader, parserOptions); - } + GraphqlAntlrToLanguage toLanguage = getAntlrToLanguage(tokens, multiSourceReader, environment); - setupParserListener(multiSourceReader, parser, toLanguage); + setupParserListener(environment, multiSourceReader, parser, toLanguage); // @@ -281,7 +339,7 @@ public void syntaxError(Recognizer recognizer, Object offendingSymbol, int return node; } - private void setupParserListener(MultiSourceReader multiSourceReader, GraphqlParser parser, GraphqlAntlrToLanguage toLanguage) { + private void setupParserListener(ParserEnvironment environment, MultiSourceReader multiSourceReader, GraphqlParser parser, GraphqlAntlrToLanguage toLanguage) { ParserOptions parserOptions = toLanguage.getParserOptions(); ParsingListener parsingListener = parserOptions.getParsingListener(); int maxTokens = parserOptions.getMaxTokens(); @@ -312,15 +370,15 @@ public int getCharPositionInLine() { count++; if (count > maxTokens) { - throwCancelParseIfTooManyTokens(token, maxTokens, multiSourceReader); + throwCancelParseIfTooManyTokens(environment, token, maxTokens, multiSourceReader); } } }; parser.addParseListener(listener); } - private void throwCancelParseIfTooManyTokens(Token token, int maxTokens, MultiSourceReader multiSourceReader) throws ParseCancelledException { - String tokenType = "grammar"; + private void throwCancelParseIfTooManyTokens(ParserEnvironment environment, Token token, int maxTokens, MultiSourceReader multiSourceReader) throws ParseCancelledException { + String tokenType = "grammar"; SourceLocation sourceLocation = null; String offendingToken = null; if (token != null) { @@ -330,23 +388,7 @@ private void throwCancelParseIfTooManyTokens(Token token, int maxTokens, MultiSo offendingToken = token.getText(); sourceLocation = AntlrHelper.createSourceLocation(multiSourceReader, token.getLine(), token.getCharPositionInLine()); } - String msg = String.format("More than %d %s tokens have been presented. To prevent Denial Of Service attacks, parsing has been cancelled.", maxTokens, tokenType); - throw new ParseCancelledException(msg, sourceLocation, offendingToken); - } - - /** - * Allows you to override the ANTLR to AST code. - * - * @param tokens the token stream - * @param multiSourceReader the source of the query document - * - * @return a new GraphqlAntlrToLanguage instance - * - * @deprecated - really should use {@link #getAntlrToLanguage(CommonTokenStream, MultiSourceReader, ParserOptions)} - */ - @Deprecated - protected GraphqlAntlrToLanguage getAntlrToLanguage(CommonTokenStream tokens, MultiSourceReader multiSourceReader) { - return null; + throw new ParseCancelledException(environment.getI18N(), sourceLocation, offendingToken, maxTokens, tokenType); } /** @@ -354,11 +396,11 @@ protected GraphqlAntlrToLanguage getAntlrToLanguage(CommonTokenStream tokens, Mu * * @param tokens the token stream * @param multiSourceReader the source of the query document - * @param parserOptions - the parser options + * @param environment the parser environment * * @return a new GraphqlAntlrToLanguage instance */ - protected GraphqlAntlrToLanguage getAntlrToLanguage(CommonTokenStream tokens, MultiSourceReader multiSourceReader, ParserOptions parserOptions) { - return new GraphqlAntlrToLanguage(tokens, multiSourceReader, parserOptions); + protected GraphqlAntlrToLanguage getAntlrToLanguage(CommonTokenStream tokens, MultiSourceReader multiSourceReader, ParserEnvironment environment) { + return new GraphqlAntlrToLanguage(tokens, multiSourceReader, environment.getParserOptions(), environment.getI18N(), null); } } diff --git a/src/main/java/graphql/parser/ParserEnvironment.java b/src/main/java/graphql/parser/ParserEnvironment.java new file mode 100644 index 0000000000..7f0ac696ba --- /dev/null +++ b/src/main/java/graphql/parser/ParserEnvironment.java @@ -0,0 +1,97 @@ +package graphql.parser; + +import graphql.PublicApi; +import graphql.i18n.I18n; + +import java.io.Reader; +import java.io.StringReader; +import java.util.Locale; + +import static graphql.Assert.assertNotNull; + +/** + * This is the arguments that can be passed to a {@link Parser} + */ +@PublicApi +public interface ParserEnvironment { + + /** + * @return the document to be parsed + */ + Reader getDocument(); + + /** + * @return the parsing options + */ + ParserOptions getParserOptions(); + + /** + * @return the locale to produce parsing error messages in + */ + Locale getLocale(); + + /** + * @return the {@link I18n} to produce parsing error messages with + */ + I18n getI18N(); + + /** + * @return a builder of new parsing options + */ + static Builder newParserEnvironment() { + return new Builder(); + } + + class Builder { + Reader reader; + ParserOptions parserOptions = ParserOptions.getDefaultParserOptions(); + Locale locale = Locale.getDefault(); + + public Builder() { + } + + public Builder document(Reader documentText) { + this.reader = assertNotNull(documentText); + return this; + } + + public Builder document(String documentText) { + return document(new StringReader(documentText)); + } + + public Builder parserOptions(ParserOptions parserOptions) { + this.parserOptions = parserOptions; + return this; + } + + public Builder locale(Locale locale) { + this.locale = assertNotNull(locale); + return this; + } + + public ParserEnvironment build() { + I18n i18n = I18n.i18n(I18n.BundleType.Parsing, locale); + return new ParserEnvironment() { + @Override + public Reader getDocument() { + return reader; + } + + @Override + public ParserOptions getParserOptions() { + return parserOptions; + } + + @Override + public Locale getLocale() { + return locale; + } + + @Override + public I18n getI18N() { + return i18n; + } + }; + } + } +} diff --git a/src/main/java/graphql/parser/StringValueParsing.java b/src/main/java/graphql/parser/StringValueParsing.java index d9da00dc83..56b1f1ec88 100644 --- a/src/main/java/graphql/parser/StringValueParsing.java +++ b/src/main/java/graphql/parser/StringValueParsing.java @@ -2,6 +2,7 @@ import graphql.Assert; import graphql.Internal; +import graphql.i18n.I18n; import graphql.language.SourceLocation; import java.io.StringWriter; @@ -103,7 +104,7 @@ private static boolean containsOnlyWhiteSpace(String str) { return leadingWhitespace(str) == str.length(); } - public static String parseSingleQuotedString(String string, SourceLocation sourceLocation) { + public static String parseSingleQuotedString(I18n i18n, String string, SourceLocation sourceLocation) { StringWriter writer = new StringWriter(string.length() - 2); int end = string.length() - 1; for (int i = 1; i < end; i++) { @@ -140,7 +141,7 @@ public static String parseSingleQuotedString(String string, SourceLocation sourc writer.write('\t'); continue; case 'u': - i = UnicodeUtil.parseAndWriteUnicode(writer, string, i, sourceLocation); + i = UnicodeUtil.parseAndWriteUnicode(i18n, writer, string, i, sourceLocation); continue; default: Assert.assertShouldNeverHappen(); @@ -148,8 +149,4 @@ public static String parseSingleQuotedString(String string, SourceLocation sourc } return writer.toString(); } - - public static String parseSingleQuotedString(String string) { - return parseSingleQuotedString(string, null); - } } diff --git a/src/main/java/graphql/parser/UnicodeUtil.java b/src/main/java/graphql/parser/UnicodeUtil.java index cdf55ace47..b32cbfea6a 100644 --- a/src/main/java/graphql/parser/UnicodeUtil.java +++ b/src/main/java/graphql/parser/UnicodeUtil.java @@ -1,7 +1,9 @@ package graphql.parser; import graphql.Internal; +import graphql.i18n.I18n; import graphql.language.SourceLocation; +import graphql.parser.exceptions.InvalidUnicodeSyntaxException; import java.io.IOException; import java.io.StringWriter; @@ -19,14 +21,14 @@ public class UnicodeUtil { public static final int TRAILING_SURROGATE_LOWER_BOUND = 0xDC00; public static final int TRAILING_SURROGATE_UPPER_BOUND = 0xDFFF; - public static int parseAndWriteUnicode(StringWriter writer, String string, int i, SourceLocation sourceLocation) { + public static int parseAndWriteUnicode(I18n i18n, StringWriter writer, String string, int i, SourceLocation sourceLocation) { // Unicode code points can either be: // 1. Unbraced: four hex characters in the form \\u597D, or // 2. Braced: any number of hex characters surrounded by braces in the form \\u{1F37A} // Extract the code point hex digits. Index i points to 'u' int startIndex = isBracedEscape(string, i) ? i + 2 : i + 1; - int endIndexExclusive = getEndIndexExclusive(string, i, sourceLocation); + int endIndexExclusive = getEndIndexExclusive(i18n, string, i, sourceLocation); // Index for parser to continue at, the last character of the escaped unicode character. Either } or hex digit int continueIndex = isBracedEscape(string, i) ? endIndexExclusive : endIndexExclusive - 1; @@ -34,16 +36,16 @@ public static int parseAndWriteUnicode(StringWriter writer, String string, int i int codePoint = Integer.parseInt(hexStr, 16); if (isTrailingSurrogateValue(codePoint)) { - throw new InvalidSyntaxException(sourceLocation, "Invalid unicode - trailing surrogate must be preceded with a leading surrogate -", null, string.substring(i - 1, continueIndex + 1), null); + throw new InvalidUnicodeSyntaxException(i18n, "InvalidUnicode.trailingLeadingSurrogate", sourceLocation, offendingToken(string, i, continueIndex)); } else if (isLeadingSurrogateValue(codePoint)) { if (!isEscapedUnicode(string, continueIndex + 1)) { - throw new InvalidSyntaxException(sourceLocation, "Invalid unicode - leading surrogate must be followed by a trailing surrogate -", null, string.substring(i - 1, continueIndex + 1), null); + throw new InvalidUnicodeSyntaxException(i18n, "InvalidUnicode.leadingTrailingSurrogate", sourceLocation, offendingToken(string, i, continueIndex)); } // Shift parser ahead to 'u' in second escaped Unicode character i = continueIndex + 2; int trailingStartIndex = isBracedEscape(string, i) ? i + 2 : i + 1; - int trailingEndIndexExclusive = getEndIndexExclusive(string, i, sourceLocation); + int trailingEndIndexExclusive = getEndIndexExclusive(i18n, string, i, sourceLocation); String trailingHexStr = string.substring(trailingStartIndex, trailingEndIndexExclusive); int trailingCodePoint = Integer.parseInt(trailingHexStr, 16); continueIndex = isBracedEscape(string, i) ? trailingEndIndexExclusive : trailingEndIndexExclusive - 1; @@ -54,16 +56,20 @@ public static int parseAndWriteUnicode(StringWriter writer, String string, int i return continueIndex; } - throw new InvalidSyntaxException(sourceLocation, "Invalid unicode - leading surrogate must be followed by a trailing surrogate -", null, string.substring(i - 1, continueIndex + 1), null); + throw new InvalidUnicodeSyntaxException(i18n, "InvalidUnicode.leadingTrailingSurrogate", sourceLocation, offendingToken(string, i, continueIndex)); } else if (isValidUnicodeCodePoint(codePoint)) { writeCodePoint(writer, codePoint); return continueIndex; } - throw new InvalidSyntaxException(sourceLocation, "Invalid unicode - not a valid code point -", null, string.substring(i - 1, continueIndex + 1), null); + throw new InvalidUnicodeSyntaxException(i18n, "InvalidUnicode.invalidCodePoint", sourceLocation, offendingToken(string, i, continueIndex)); } - private static int getEndIndexExclusive(String string, int i, SourceLocation sourceLocation) { + private static String offendingToken(String string, int i, int continueIndex) { + return string.substring(i - 1, continueIndex + 1); + } + + private static int getEndIndexExclusive(I18n i18n, String string, int i, SourceLocation sourceLocation) { // Unbraced case, with exactly 4 hex digits if (string.length() > i + 5 && !isBracedEscape(string, i)) { return i + 5; @@ -73,7 +79,7 @@ private static int getEndIndexExclusive(String string, int i, SourceLocation sou int endIndexExclusive = i + 2; do { if (endIndexExclusive + 1 >= string.length()) { - throw new InvalidSyntaxException(sourceLocation, "Invalid unicode - incorrectly formatted escape -", null, string.substring(i - 1, endIndexExclusive), null); + throw new InvalidUnicodeSyntaxException(i18n, "InvalidUnicode.incorrectEscape", sourceLocation, string.substring(i - 1, endIndexExclusive)); } } while (string.charAt(++endIndexExclusive) != '}'); diff --git a/src/main/java/graphql/parser/exceptions/InvalidUnicodeSyntaxException.java b/src/main/java/graphql/parser/exceptions/InvalidUnicodeSyntaxException.java new file mode 100644 index 0000000000..be75127047 --- /dev/null +++ b/src/main/java/graphql/parser/exceptions/InvalidUnicodeSyntaxException.java @@ -0,0 +1,16 @@ +package graphql.parser.exceptions; + +import graphql.Internal; +import graphql.i18n.I18n; +import graphql.language.SourceLocation; +import graphql.parser.InvalidSyntaxException; +import org.jetbrains.annotations.NotNull; + +@Internal +public class InvalidUnicodeSyntaxException extends InvalidSyntaxException { + + public InvalidUnicodeSyntaxException(@NotNull I18n i18N, @NotNull String msgKey, @NotNull SourceLocation sourceLocation, @NotNull String offendingToken) { + super(i18N.msg(msgKey, offendingToken, sourceLocation.getLine(), sourceLocation.getColumn()), + sourceLocation, offendingToken, null, null); + } +} diff --git a/src/main/java/graphql/parser/exceptions/MoreTokensSyntaxException.java b/src/main/java/graphql/parser/exceptions/MoreTokensSyntaxException.java new file mode 100644 index 0000000000..6f73b38e4c --- /dev/null +++ b/src/main/java/graphql/parser/exceptions/MoreTokensSyntaxException.java @@ -0,0 +1,17 @@ +package graphql.parser.exceptions; + +import graphql.Internal; +import graphql.i18n.I18n; +import graphql.language.SourceLocation; +import graphql.parser.InvalidSyntaxException; +import org.jetbrains.annotations.NotNull; + +@Internal +public class MoreTokensSyntaxException extends InvalidSyntaxException { + + @Internal + public MoreTokensSyntaxException(@NotNull I18n i18N, @NotNull SourceLocation sourceLocation, @NotNull String offendingToken, @NotNull String sourcePreview) { + super(i18N.msg("InvalidSyntaxMoreTokens.full", offendingToken, sourceLocation.getLine(), sourceLocation.getColumn()), + sourceLocation, offendingToken, sourcePreview, null); + } +} diff --git a/src/main/java/graphql/parser/exceptions/ParseCancelledException.java b/src/main/java/graphql/parser/exceptions/ParseCancelledException.java new file mode 100644 index 0000000000..ab183367f3 --- /dev/null +++ b/src/main/java/graphql/parser/exceptions/ParseCancelledException.java @@ -0,0 +1,18 @@ +package graphql.parser.exceptions; + +import graphql.Internal; +import graphql.i18n.I18n; +import graphql.language.SourceLocation; +import graphql.parser.InvalidSyntaxException; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +@Internal +public class ParseCancelledException extends InvalidSyntaxException { + + @Internal + public ParseCancelledException(@NotNull I18n i18N, @Nullable SourceLocation sourceLocation, @Nullable String offendingToken, int maxTokens, @NotNull String tokenType) { + super(i18N.msg("ParseCancelled.full", maxTokens, tokenType), + sourceLocation, offendingToken, null, null); + } +} diff --git a/src/main/java/graphql/relay/DefaultConnection.java b/src/main/java/graphql/relay/DefaultConnection.java index 42729d898e..e6db4dc4ea 100644 --- a/src/main/java/graphql/relay/DefaultConnection.java +++ b/src/main/java/graphql/relay/DefaultConnection.java @@ -5,6 +5,7 @@ import java.util.Collections; import java.util.List; +import java.util.Objects; import static graphql.Assert.assertNotNull; @@ -40,6 +41,23 @@ public PageInfo getPageInfo() { return pageInfo; } + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + DefaultConnection that = (DefaultConnection) o; + return Objects.equals(edges, that.edges) && Objects.equals(pageInfo, that.pageInfo); + } + + @Override + public int hashCode() { + return Objects.hash(edges, pageInfo); + } + @Override public String toString() { return "DefaultConnection{" + diff --git a/src/main/java/graphql/relay/DefaultEdge.java b/src/main/java/graphql/relay/DefaultEdge.java index eb29b66e19..e43f4c54ce 100644 --- a/src/main/java/graphql/relay/DefaultEdge.java +++ b/src/main/java/graphql/relay/DefaultEdge.java @@ -2,6 +2,8 @@ import graphql.PublicApi; +import java.util.Objects; + import static graphql.Assert.assertNotNull; @PublicApi @@ -26,6 +28,23 @@ public ConnectionCursor getCursor() { return cursor; } + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + DefaultEdge that = (DefaultEdge) o; + return Objects.equals(node, that.node) && Objects.equals(cursor, that.cursor); + } + + @Override + public int hashCode() { + return Objects.hash(node, cursor); + } + @Override public String toString() { return "DefaultEdge{" + diff --git a/src/main/java/graphql/relay/DefaultPageInfo.java b/src/main/java/graphql/relay/DefaultPageInfo.java index 6e20d073c2..d76ef04fb1 100644 --- a/src/main/java/graphql/relay/DefaultPageInfo.java +++ b/src/main/java/graphql/relay/DefaultPageInfo.java @@ -3,6 +3,8 @@ import graphql.PublicApi; +import java.util.Objects; + @PublicApi public class DefaultPageInfo implements PageInfo { @@ -39,6 +41,26 @@ public boolean isHasNextPage() { return hasNextPage; } + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + DefaultPageInfo that = (DefaultPageInfo) o; + return Objects.equals(startCursor, that.startCursor) && + Objects.equals(endCursor, that.endCursor) && + Objects.equals(hasPreviousPage, that.hasPreviousPage) && + Objects.equals(hasNextPage, that.hasNextPage); + } + + @Override + public int hashCode() { + return Objects.hash(startCursor, endCursor, hasPreviousPage, hasNextPage); + } + @Override public String toString() { return "DefaultPageInfo{" + diff --git a/src/main/java/graphql/scalar/CoercingUtil.java b/src/main/java/graphql/scalar/CoercingUtil.java index 7464d3161d..c3ac535522 100644 --- a/src/main/java/graphql/scalar/CoercingUtil.java +++ b/src/main/java/graphql/scalar/CoercingUtil.java @@ -1,18 +1,25 @@ package graphql.scalar; import graphql.Internal; +import graphql.i18n.I18n; + +import java.util.Locale; @Internal -class CoercingUtil { - static boolean isNumberIsh(Object input) { +public class CoercingUtil { + public static boolean isNumberIsh(Object input) { return input instanceof Number || input instanceof String; } - static String typeName(Object input) { + public static String typeName(Object input) { if (input == null) { return "null"; } return input.getClass().getSimpleName(); } + + public static String i18nMsg(Locale locale, String msgKey, Object... args) { + return I18n.i18n(I18n.BundleType.Scalars, locale).msg(msgKey, args); + } } diff --git a/src/main/java/graphql/scalar/GraphqlBooleanCoercing.java b/src/main/java/graphql/scalar/GraphqlBooleanCoercing.java index d4b01750af..6ef64c5976 100644 --- a/src/main/java/graphql/scalar/GraphqlBooleanCoercing.java +++ b/src/main/java/graphql/scalar/GraphqlBooleanCoercing.java @@ -1,20 +1,30 @@ package graphql.scalar; +import graphql.GraphQLContext; import graphql.Internal; +import graphql.execution.CoercedVariables; import graphql.language.BooleanValue; import graphql.language.Value; import graphql.schema.Coercing; import graphql.schema.CoercingParseLiteralException; import graphql.schema.CoercingParseValueException; import graphql.schema.CoercingSerializeException; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; import java.math.BigDecimal; +import java.util.Locale; import static graphql.Assert.assertNotNull; import static graphql.Assert.assertShouldNeverHappen; +import static graphql.scalar.CoercingUtil.i18nMsg; import static graphql.scalar.CoercingUtil.isNumberIsh; import static graphql.scalar.CoercingUtil.typeName; +/** + * The deprecated methods still have implementations in case code outside graphql-java is calling them + * but internally the call paths have been replaced. + */ @Internal public class GraphqlBooleanCoercing implements Coercing { @@ -45,41 +55,83 @@ private Boolean convertImpl(Object input) { } - @Override - public Boolean serialize(Object input) { + @NotNull + private Boolean serializeImpl(@NotNull Object input, @NotNull Locale locale) { Boolean result = convertImpl(input); if (result == null) { throw new CoercingSerializeException( - "Expected type 'Boolean' but was '" + typeName(input) + "'." + i18nMsg(locale, "Boolean.notBoolean", typeName(input)) ); } return result; } - @Override - public Boolean parseValue(Object input) { - Boolean result = convertImpl(input); - if (result == null) { + @NotNull + private Boolean parseValueImpl(@NotNull Object input, @NotNull Locale locale) { + if (!(input instanceof Boolean)) { throw new CoercingParseValueException( - "Expected type 'Boolean' but was '" + typeName(input) + "'." + i18nMsg(locale, "Boolean.unexpectedRawValueType", typeName(input)) ); } - return result; + return (Boolean) input; } - @Override - public Boolean parseLiteral(Object input) { + private static boolean parseLiteralImpl(@NotNull Object input, @NotNull Locale locale) { if (!(input instanceof BooleanValue)) { throw new CoercingParseLiteralException( - "Expected AST type 'BooleanValue' but was '" + typeName(input) + "'." + i18nMsg(locale, "Boolean.unexpectedAstType", typeName(input)) ); } return ((BooleanValue) input).isValue(); } - @Override - public Value valueToLiteral(Object input) { - Boolean result = assertNotNull(convertImpl(input)); + @NotNull + private BooleanValue valueToLiteralImpl(@NotNull Object input, @NotNull Locale locale) { + Boolean result = assertNotNull(convertImpl(input), () -> i18nMsg(locale, "Boolean.notBoolean", typeName(input))); return BooleanValue.newBooleanValue(result).build(); } + + @Override + @Deprecated + public Boolean serialize(@NotNull Object dataFetcherResult) { + return serializeImpl(dataFetcherResult, Locale.getDefault()); + } + + @Override + public @Nullable Boolean serialize(@NotNull Object dataFetcherResult, @NotNull GraphQLContext graphQLContext, @NotNull Locale locale) throws CoercingSerializeException { + return serializeImpl(dataFetcherResult, locale); + } + + @Override + @Deprecated + public Boolean parseValue(@NotNull Object input) { + return parseValueImpl(input, Locale.getDefault()); + } + + @Override + public Boolean parseValue(@NotNull Object input, @NotNull GraphQLContext graphQLContext, @NotNull Locale locale) throws CoercingParseValueException { + return parseValueImpl(input, locale); + } + + @Override + @Deprecated + public Boolean parseLiteral(@NotNull Object input) { + return parseLiteralImpl(input, Locale.getDefault()); + } + + @Override + public @Nullable Boolean parseLiteral(@NotNull Value input, @NotNull CoercedVariables variables, @NotNull GraphQLContext graphQLContext, @NotNull Locale locale) throws CoercingParseLiteralException { + return parseLiteralImpl(input, locale); + } + + @Override + @Deprecated + public Value valueToLiteral(@NotNull Object input) { + return valueToLiteralImpl(input, Locale.getDefault()); + } + + @Override + public @NotNull Value valueToLiteral(@NotNull Object input, @NotNull GraphQLContext graphQLContext, @NotNull Locale locale) { + return valueToLiteralImpl(input, locale); + } } diff --git a/src/main/java/graphql/scalar/GraphqlFloatCoercing.java b/src/main/java/graphql/scalar/GraphqlFloatCoercing.java index c84fd538b2..683f915634 100644 --- a/src/main/java/graphql/scalar/GraphqlFloatCoercing.java +++ b/src/main/java/graphql/scalar/GraphqlFloatCoercing.java @@ -1,6 +1,8 @@ package graphql.scalar; +import graphql.GraphQLContext; import graphql.Internal; +import graphql.execution.CoercedVariables; import graphql.language.FloatValue; import graphql.language.IntValue; import graphql.language.Value; @@ -8,71 +10,137 @@ import graphql.schema.CoercingParseLiteralException; import graphql.schema.CoercingParseValueException; import graphql.schema.CoercingSerializeException; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; import java.math.BigDecimal; +import java.util.Locale; import static graphql.Assert.assertNotNull; +import static graphql.scalar.CoercingUtil.i18nMsg; import static graphql.scalar.CoercingUtil.isNumberIsh; import static graphql.scalar.CoercingUtil.typeName; +/** + * The deprecated methods still have implementations in case code outside graphql-java is calling them + * but internally the call paths have been replaced. + */ @Internal public class GraphqlFloatCoercing implements Coercing { private Double convertImpl(Object input) { - if (isNumberIsh(input)) { + // From the GraphQL Float spec, non-finite floating-point internal values (NaN and Infinity) + // must raise a field error on both result and input coercion + Double doubleInput; + if (input instanceof Double) { + doubleInput = (Double) input; + } else if (isNumberIsh(input)) { BigDecimal value; try { value = new BigDecimal(input.toString()); } catch (NumberFormatException e) { return null; } - return value.doubleValue(); + doubleInput = value.doubleValue(); } else { return null; } + if (Double.isNaN(doubleInput) || Double.isInfinite(doubleInput)) { + return null; + } + return doubleInput; } - @Override - public Double serialize(Object input) { + @NotNull + private Double serialiseImpl(Object input, @NotNull Locale locale) { Double result = convertImpl(input); if (result == null) { throw new CoercingSerializeException( - "Expected type 'Float' but was '" + typeName(input) + "'." + i18nMsg(locale, "Float.notFloat", typeName(input)) ); } return result; - } - @Override - public Double parseValue(Object input) { + @NotNull + private Double parseValueImpl(@NotNull Object input, @NotNull Locale locale) { + if (!(input instanceof Number)) { + throw new CoercingParseValueException( + i18nMsg(locale, "Float.unexpectedRawValueType", typeName(input)) + ); + } + Double result = convertImpl(input); if (result == null) { throw new CoercingParseValueException( - "Expected type 'Float' but was '" + typeName(input) + "'." + i18nMsg(locale, "Float.notFloat", typeName(input)) ); } + return result; } - @Override - public Double parseLiteral(Object input) { + private static double parseLiteralImpl(@NotNull Object input, @NotNull Locale locale) { if (input instanceof IntValue) { return ((IntValue) input).getValue().doubleValue(); } else if (input instanceof FloatValue) { return ((FloatValue) input).getValue().doubleValue(); } else { throw new CoercingParseLiteralException( - "Expected AST type 'IntValue' or 'FloatValue' but was '" + typeName(input) + "'." + i18nMsg(locale, "Float.unexpectedAstType", typeName(input)) ); } } - @Override - public Value valueToLiteral(Object input) { - Double result = assertNotNull(convertImpl(input)); + @NotNull + private FloatValue valueToLiteralImpl(Object input, @NotNull Locale locale) { + Double result = assertNotNull(convertImpl(input), () -> i18nMsg(locale, "Float.notFloat", typeName(input))); return FloatValue.newFloatValue(BigDecimal.valueOf(result)).build(); } + + @Override + @Deprecated + public Double serialize(@NotNull Object dataFetcherResult) { + return serialiseImpl(dataFetcherResult, Locale.getDefault()); + } + + @Override + public @Nullable Double serialize(@NotNull Object dataFetcherResult, @NotNull GraphQLContext graphQLContext, @NotNull Locale locale) throws CoercingSerializeException { + return serialiseImpl(dataFetcherResult, locale); + } + + @Override + @Deprecated + public @NotNull Double parseValue(@NotNull Object input) { + return parseValueImpl(input, Locale.getDefault()); + } + + @Override + public Double parseValue(@NotNull Object input, @NotNull GraphQLContext graphQLContext, @NotNull Locale locale) throws CoercingParseValueException { + return parseValueImpl(input, locale); + } + + @Override + @Deprecated + public Double parseLiteral(@NotNull Object input) { + return parseLiteralImpl(input, Locale.getDefault()); + } + + @Override + public @Nullable Double parseLiteral(@NotNull Value input, @NotNull CoercedVariables variables, @NotNull GraphQLContext graphQLContext, @NotNull Locale locale) throws CoercingParseLiteralException { + return parseLiteralImpl(input, locale); + } + + @Override + @Deprecated + public Value valueToLiteral(@NotNull Object input) { + return valueToLiteralImpl(input, Locale.getDefault()); + } + + @Override + public @NotNull Value valueToLiteral(@NotNull Object input, @NotNull GraphQLContext graphQLContext, @NotNull Locale locale) { + return valueToLiteralImpl(input, locale); + } } diff --git a/src/main/java/graphql/scalar/GraphqlIDCoercing.java b/src/main/java/graphql/scalar/GraphqlIDCoercing.java index 69f8ed8b92..76e78e7917 100644 --- a/src/main/java/graphql/scalar/GraphqlIDCoercing.java +++ b/src/main/java/graphql/scalar/GraphqlIDCoercing.java @@ -1,6 +1,8 @@ package graphql.scalar; +import graphql.GraphQLContext; import graphql.Internal; +import graphql.execution.CoercedVariables; import graphql.language.IntValue; import graphql.language.StringValue; import graphql.language.Value; @@ -8,13 +10,21 @@ import graphql.schema.CoercingParseLiteralException; import graphql.schema.CoercingParseValueException; import graphql.schema.CoercingSerializeException; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; import java.math.BigInteger; +import java.util.Locale; import java.util.UUID; import static graphql.Assert.assertNotNull; +import static graphql.scalar.CoercingUtil.i18nMsg; import static graphql.scalar.CoercingUtil.typeName; +/** + * The deprecated methods still have implementations in case code outside graphql-java is calling them + * but internally the call paths have been replaced. + */ @Internal public class GraphqlIDCoercing implements Coercing { @@ -38,8 +48,8 @@ private String convertImpl(Object input) { } - @Override - public String serialize(Object input) { + @NotNull + private String serializeImpl(Object input, @NotNull Locale locale) { String result = String.valueOf(input); if (result == null) { throw new CoercingSerializeException( @@ -49,19 +59,18 @@ public String serialize(Object input) { return result; } - @Override - public String parseValue(Object input) { + @NotNull + private String parseValueImpl(Object input, @NotNull Locale locale) { String result = convertImpl(input); if (result == null) { throw new CoercingParseValueException( - "Expected type 'ID' but was '" + typeName(input) + "'." + i18nMsg(locale, "ID.notId", typeName(input)) ); } return result; } - @Override - public String parseLiteral(Object input) { + private String parseLiteralImpl(Object input, @NotNull Locale locale) { if (input instanceof StringValue) { return ((StringValue) input).getValue(); } @@ -69,13 +78,57 @@ public String parseLiteral(Object input) { return ((IntValue) input).getValue().toString(); } throw new CoercingParseLiteralException( - "Expected AST type 'IntValue' or 'StringValue' but was '" + typeName(input) + "'." + i18nMsg(locale, "ID.unexpectedAstType", typeName(input)) ); } - @Override - public Value valueToLiteral(Object input) { - String result = assertNotNull(convertImpl(input)); + @NotNull + private StringValue valueToLiteralImpl(Object input, @NotNull Locale locale) { + String result = assertNotNull(convertImpl(input), () -> i18nMsg(locale, "ID.notId", typeName(input))); return StringValue.newStringValue(result).build(); } + + @Override + @Deprecated + public String serialize(@NotNull Object dataFetcherResult) { + return serializeImpl(dataFetcherResult, Locale.getDefault()); + } + + @Override + public @Nullable Object serialize(@NotNull Object dataFetcherResult, @NotNull GraphQLContext graphQLContext, @NotNull Locale locale) throws CoercingSerializeException { + return serializeImpl(dataFetcherResult, locale); + } + + @Override + @Deprecated + public String parseValue(@NotNull Object input) { + return parseValueImpl(input, Locale.getDefault()); + } + + @Override + public Object parseValue(@NotNull Object input, @NotNull GraphQLContext graphQLContext, @NotNull Locale locale) throws CoercingParseValueException { + return parseValueImpl(input, locale); + } + + @Override + @Deprecated + public String parseLiteral(@NotNull Object input) { + return parseLiteralImpl(input, Locale.getDefault()); + } + + @Override + public @Nullable Object parseLiteral(@NotNull Value input, @NotNull CoercedVariables variables, @NotNull GraphQLContext graphQLContext, @NotNull Locale locale) throws CoercingParseLiteralException { + return parseLiteralImpl(input, locale); + } + + @Override + @Deprecated + public Value valueToLiteral(@NotNull Object input) { + return valueToLiteralImpl(input, Locale.getDefault()); + } + + @Override + public @NotNull Value valueToLiteral(@NotNull Object input, @NotNull GraphQLContext graphQLContext, @NotNull Locale locale) { + return valueToLiteralImpl(input, locale); + } } diff --git a/src/main/java/graphql/scalar/GraphqlIntCoercing.java b/src/main/java/graphql/scalar/GraphqlIntCoercing.java index a828019688..350c4f4814 100644 --- a/src/main/java/graphql/scalar/GraphqlIntCoercing.java +++ b/src/main/java/graphql/scalar/GraphqlIntCoercing.java @@ -1,20 +1,30 @@ package graphql.scalar; +import graphql.GraphQLContext; import graphql.Internal; +import graphql.execution.CoercedVariables; import graphql.language.IntValue; import graphql.language.Value; import graphql.schema.Coercing; import graphql.schema.CoercingParseLiteralException; import graphql.schema.CoercingParseValueException; import graphql.schema.CoercingSerializeException; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; import java.math.BigDecimal; import java.math.BigInteger; +import java.util.Locale; import static graphql.Assert.assertNotNull; +import static graphql.scalar.CoercingUtil.i18nMsg; import static graphql.scalar.CoercingUtil.isNumberIsh; import static graphql.scalar.CoercingUtil.typeName; +/** + * The deprecated methods still have implementations in case code outside graphql-java is calling them + * but internally the call paths have been replaced. + */ @Internal public class GraphqlIntCoercing implements Coercing { @@ -41,47 +51,122 @@ private Integer convertImpl(Object input) { } } - @Override - public Integer serialize(Object input) { + @NotNull + private Integer serialiseImpl(Object input, @NotNull Locale locale) { Integer result = convertImpl(input); if (result == null) { throw new CoercingSerializeException( - "Expected type 'Int' but was '" + typeName(input) + "'." + i18nMsg(locale, "Int.notInt", typeName(input)) ); } return result; } - @Override - public Integer parseValue(Object input) { - Integer result = convertImpl(input); + @NotNull + private Integer parseValueImpl(@NotNull Object input, @NotNull Locale locale) { + if (!(input instanceof Number)) { + throw new CoercingParseValueException( + i18nMsg(locale, "Int.notInt", typeName(input)) + ); + } + + if (input instanceof Integer) { + return (Integer) input; + } + + BigInteger result = convertParseValueImpl(input); if (result == null) { throw new CoercingParseValueException( - "Expected type 'Int' but was '" + typeName(input) + "'." + i18nMsg(locale, "Int.notInt", typeName(input)) ); } - return result; + + if (result.compareTo(INT_MIN) < 0 || result.compareTo(INT_MAX) > 0) { + throw new CoercingParseValueException( + i18nMsg(locale, "Int.outsideRange", result.toString()) + ); + } + return result.intValueExact(); } - @Override - public Integer parseLiteral(Object input) { + private BigInteger convertParseValueImpl(Object input) { + BigDecimal value; + try { + value = new BigDecimal(input.toString()); + } catch (NumberFormatException e) { + return null; + } + + try { + return value.toBigIntegerExact(); + } catch (ArithmeticException e) { + // Exception if number has non-zero fractional part + return null; + } + } + + private static int parseLiteralImpl(Object input, @NotNull Locale locale) { if (!(input instanceof IntValue)) { throw new CoercingParseLiteralException( - "Expected AST type 'IntValue' but was '" + typeName(input) + "'." + i18nMsg(locale, "Scalar.unexpectedAstType", "IntValue", typeName(input)) ); } BigInteger value = ((IntValue) input).getValue(); if (value.compareTo(INT_MIN) < 0 || value.compareTo(INT_MAX) > 0) { throw new CoercingParseLiteralException( - "Expected value to be in the Integer range but it was '" + value.toString() + "'" + i18nMsg(locale, "Int.outsideRange", value.toString()) ); } return value.intValue(); } - @Override - public Value valueToLiteral(Object input) { - Integer result = assertNotNull(convertImpl(input)); + private IntValue valueToLiteralImpl(Object input, @NotNull Locale locale) { + Integer result = assertNotNull(convertImpl(input),() -> i18nMsg(locale, "Int.notInt", typeName(input))); return IntValue.newIntValue(BigInteger.valueOf(result)).build(); } + + + @Override + @Deprecated + public Integer serialize(@NotNull Object dataFetcherResult) { + return serialiseImpl(dataFetcherResult, Locale.getDefault()); + } + + @Override + public @Nullable Integer serialize(@NotNull Object dataFetcherResult, @NotNull GraphQLContext graphQLContext, @NotNull Locale locale) throws CoercingSerializeException { + return serialiseImpl(dataFetcherResult, locale); + } + + @Override + @Deprecated + public Integer parseValue(@NotNull Object input) { + return parseValueImpl(input, Locale.getDefault()); + } + + @Override + public Integer parseValue(@NotNull Object input, @NotNull GraphQLContext graphQLContext, @NotNull Locale locale) throws CoercingParseValueException { + return parseValueImpl(input, locale); + } + + @Override + @Deprecated + public Integer parseLiteral(@NotNull Object input) { + return parseLiteralImpl(input, Locale.getDefault()); + } + + @Override + public @Nullable Integer parseLiteral(@NotNull Value input, @NotNull CoercedVariables variables, @NotNull GraphQLContext graphQLContext, @NotNull Locale locale) throws CoercingParseLiteralException { + return parseLiteralImpl(input, locale); + } + + @Override + @Deprecated + public Value valueToLiteral(@NotNull Object input) { + return valueToLiteralImpl(input, Locale.getDefault()); + } + + @Override + public @NotNull Value valueToLiteral(@NotNull Object input, @NotNull GraphQLContext graphQLContext, @NotNull Locale locale) { + return valueToLiteralImpl(input, locale); + } } diff --git a/src/main/java/graphql/scalar/GraphqlStringCoercing.java b/src/main/java/graphql/scalar/GraphqlStringCoercing.java index 7bddeba04d..b330f254ae 100644 --- a/src/main/java/graphql/scalar/GraphqlStringCoercing.java +++ b/src/main/java/graphql/scalar/GraphqlStringCoercing.java @@ -1,37 +1,96 @@ package graphql.scalar; +import graphql.GraphQLContext; import graphql.Internal; +import graphql.execution.CoercedVariables; import graphql.language.StringValue; import graphql.language.Value; import graphql.schema.Coercing; import graphql.schema.CoercingParseLiteralException; +import graphql.schema.CoercingParseValueException; +import graphql.schema.CoercingSerializeException; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; +import java.util.Locale; + +import static graphql.scalar.CoercingUtil.i18nMsg; import static graphql.scalar.CoercingUtil.typeName; +/** + * The deprecated methods still have implementations in case code outside graphql-java is calling them + * but internally the call paths have been replaced. + */ @Internal public class GraphqlStringCoercing implements Coercing { - @Override - public String serialize(Object input) { - return input.toString(); + + private String toStringImpl(Object input) { + return String.valueOf(input); } - @Override - public String parseValue(Object input) { - return serialize(input); + private String parseValueImpl(@NotNull Object input, @NotNull Locale locale) { + if (!(input instanceof String)) { + throw new CoercingParseValueException( + i18nMsg(locale, "String.unexpectedRawValueType", typeName(input)) + ); + } + return (String) input; } - @Override - public String parseLiteral(Object input) { + private String parseLiteralImpl(@NotNull Object input, Locale locale) { if (!(input instanceof StringValue)) { throw new CoercingParseLiteralException( - "Expected AST type 'StringValue' but was '" + typeName(input) + "'." + i18nMsg(locale, "Scalar.unexpectedAstType", "StringValue", typeName(input)) ); } return ((StringValue) input).getValue(); } - @Override - public Value valueToLiteral(Object input) { + private StringValue valueToLiteralImpl(@NotNull Object input) { return StringValue.newStringValue(input.toString()).build(); } + + @Override + @Deprecated + public String serialize(@NotNull Object dataFetcherResult) { + return toStringImpl(dataFetcherResult); + } + + @Override + public @Nullable String serialize(@NotNull Object dataFetcherResult, @NotNull GraphQLContext graphQLContext, @NotNull Locale locale) throws CoercingSerializeException { + return toStringImpl(dataFetcherResult); + } + + @Override + @Deprecated + public String parseValue(@NotNull Object input) { + return parseValueImpl(input, Locale.getDefault()); + } + + @Override + public String parseValue(@NotNull Object input, @NotNull GraphQLContext graphQLContext, @NotNull Locale locale) throws CoercingParseValueException { + return parseValueImpl(input, locale); + } + + @Override + @Deprecated + public String parseLiteral(@NotNull Object input) { + return parseLiteralImpl(input, Locale.getDefault()); + } + + @Override + public @Nullable String parseLiteral(@NotNull Value input, @NotNull CoercedVariables variables, @NotNull GraphQLContext graphQLContext, @NotNull Locale locale) throws CoercingParseLiteralException { + return parseLiteralImpl(input, locale); + } + + @Override + @Deprecated + public Value valueToLiteral(@NotNull Object input) { + return valueToLiteralImpl(input); + } + + @Override + public @NotNull Value valueToLiteral(@NotNull Object input, @NotNull GraphQLContext graphQLContext, @NotNull Locale locale) { + return valueToLiteralImpl(input); + } } diff --git a/src/main/java/graphql/schema/CodeRegistryVisitor.java b/src/main/java/graphql/schema/CodeRegistryVisitor.java index 66af22f209..50cfa2a492 100644 --- a/src/main/java/graphql/schema/CodeRegistryVisitor.java +++ b/src/main/java/graphql/schema/CodeRegistryVisitor.java @@ -2,16 +2,6 @@ import graphql.Internal; import graphql.introspection.Introspection; -import graphql.schema.DataFetcher; -import graphql.schema.FieldCoordinates; -import graphql.schema.GraphQLCodeRegistry; -import graphql.schema.GraphQLFieldDefinition; -import graphql.schema.GraphQLFieldsContainer; -import graphql.schema.GraphQLInterfaceType; -import graphql.schema.GraphQLSchemaElement; -import graphql.schema.GraphQLTypeVisitorStub; -import graphql.schema.GraphQLUnionType; -import graphql.schema.TypeResolver; import graphql.util.TraversalControl; import graphql.util.TraverserContext; diff --git a/src/main/java/graphql/schema/Coercing.java b/src/main/java/graphql/schema/Coercing.java index 79b62b959c..491bd16b0e 100644 --- a/src/main/java/graphql/schema/Coercing.java +++ b/src/main/java/graphql/schema/Coercing.java @@ -1,14 +1,21 @@ package graphql.schema; +import graphql.DeprecatedAt; +import graphql.GraphQLContext; import graphql.PublicSpi; +import graphql.execution.CoercedVariables; import graphql.language.Value; import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; +import java.util.Locale; import java.util.Map; +import static graphql.Assert.assertNotNull; + /** - * The Coercing interface is used by {@link graphql.schema.GraphQLScalarType}s to parse and serialise object values. + * The Coercing interface is used by {@link graphql.schema.GraphQLScalarType}s to parse and serialize object values. *

* There are two major responsibilities, result coercion and input coercion. *

@@ -34,18 +41,43 @@ public interface Coercing { * Called to convert a Java object result of a DataFetcher to a valid runtime value for the scalar type. *

* Note : Throw {@link graphql.schema.CoercingSerializeException} if there is fundamental - * problem during serialisation, don't return null to indicate failure. + * problem during serialization, don't return null to indicate failure. + *

+ * Note : You should not allow {@link java.lang.RuntimeException}s to come out of your serialize method, but rather + * catch them and fire them as {@link graphql.schema.CoercingSerializeException} instead as per the method contract. + * + * @param dataFetcherResult is never null + * + * @return a serialized value which may be null. + * + * @throws graphql.schema.CoercingSerializeException if value input can't be serialized + */ + @Deprecated + @DeprecatedAt("2022-08-22") + @Nullable O serialize(@NotNull Object dataFetcherResult) throws CoercingSerializeException; + + /** + * Called to convert a Java object result of a DataFetcher to a valid runtime value for the scalar type. + *

+ * Note : Throw {@link graphql.schema.CoercingSerializeException} if there is fundamental + * problem during serialization, don't return null to indicate failure. *

* Note : You should not allow {@link java.lang.RuntimeException}s to come out of your serialize method, but rather * catch them and fire them as {@link graphql.schema.CoercingSerializeException} instead as per the method contract. * * @param dataFetcherResult is never null + * @param graphQLContext the graphql context in place + * @param locale the locale to use * * @return a serialized value which may be null. * * @throws graphql.schema.CoercingSerializeException if value input can't be serialized */ - O serialize(@NotNull Object dataFetcherResult) throws CoercingSerializeException; + default @Nullable O serialize(@NotNull Object dataFetcherResult, @NotNull GraphQLContext graphQLContext, @NotNull Locale locale) throws CoercingSerializeException { + assertNotNull(dataFetcherResult); + assertNotNull(graphQLContext); + return serialize(dataFetcherResult); + } /** * Called to resolve an input from a query variable into a Java object acceptable for the scalar type. @@ -53,13 +85,40 @@ public interface Coercing { * Note : You should not allow {@link java.lang.RuntimeException}s to come out of your parseValue method, but rather * catch them and fire them as {@link graphql.schema.CoercingParseValueException} instead as per the method contract. * + * Note : if input is explicit/raw value null, input coercion will return null before this method is called + * * @param input is never null * - * @return a parsed value which is never null + * @return a parsed value which may be null + * + * @throws graphql.schema.CoercingParseValueException if value input can't be parsed + */ + @Deprecated + @DeprecatedAt("2022-08-22") + @Nullable I parseValue(@NotNull Object input) throws CoercingParseValueException; + + /** + * Called to resolve an input from a query variable into a Java object acceptable for the scalar type. + *

+ * Note : You should not allow {@link java.lang.RuntimeException}s to come out of your parseValue method, but rather + * catch them and fire them as {@link graphql.schema.CoercingParseValueException} instead as per the method contract. + * + * Note : if input is explicit/raw value null, input coercion will return null before this method is called + * + * @param input is never null + * @param graphQLContext the graphql context in place + * @param locale the locale to use + * + * @return a parsed value which may be null * * @throws graphql.schema.CoercingParseValueException if value input can't be parsed */ - @NotNull I parseValue(@NotNull Object input) throws CoercingParseValueException; + @Nullable default I parseValue(@NotNull Object input, @NotNull GraphQLContext graphQLContext, @NotNull Locale locale) throws CoercingParseValueException { + assertNotNull(input); + assertNotNull(graphQLContext); + assertNotNull(locale); + return parseValue(input); + } /** * Called during query validation to convert a query input AST node into a Java object acceptable for the scalar type. The input @@ -68,13 +127,17 @@ public interface Coercing { * Note : You should not allow {@link java.lang.RuntimeException}s to come out of your parseLiteral method, but rather * catch them and fire them as {@link graphql.schema.CoercingParseLiteralException} instead as per the method contract. * + * Note : if input is literal {@link graphql.language.NullValue}, input coercion will return null before this method is called + * * @param input is never null * - * @return a parsed value which is never null + * @return a parsed value which may be null * * @throws graphql.schema.CoercingParseLiteralException if input literal can't be parsed */ - @NotNull I parseLiteral(@NotNull Object input) throws CoercingParseLiteralException; + @Deprecated + @DeprecatedAt("2022-08-22") + @Nullable I parseLiteral(@NotNull Object input) throws CoercingParseLiteralException; /** * Called during query execution to convert a query input AST node into a Java object acceptable for the scalar type. The input @@ -87,29 +150,82 @@ public interface Coercing { * objects and convert them into actual values. But for those scalar types that want to do this, then this * method should be implemented. * + * Note : if input is literal {@link graphql.language.NullValue}, input coercion will return null before this method is called + * * @param input is never null * @param variables the resolved variables passed to the query * - * @return a parsed value which is never null + * @return a parsed value which may be null * * @throws graphql.schema.CoercingParseLiteralException if input literal can't be parsed */ @SuppressWarnings("unused") - default @NotNull I parseLiteral(Object input, Map variables) throws CoercingParseLiteralException { + @Deprecated + @DeprecatedAt("2022-08-22") + default @Nullable I parseLiteral(Object input, Map variables) throws CoercingParseLiteralException { return parseLiteral(input); } + /** + * Called during query execution to convert a query input AST node into a Java object acceptable for the scalar type. The input + * object will be an instance of {@link graphql.language.Value}. + *

+ * Note : You should not allow {@link java.lang.RuntimeException}s to come out of your parseLiteral method, but rather + * catch them and fire them as {@link graphql.schema.CoercingParseLiteralException} instead as per the method contract. + *

+ * Many scalar types don't need to implement this method because they don't take AST {@link graphql.language.VariableReference} + * objects and convert them into actual values. But for those scalar types that want to do this, then this + * method should be implemented. + * + * Note : if input is literal {@link graphql.language.NullValue}, input coercion will return null before this method is called + * + * @param input is never null + * @param variables the resolved variables passed to the query + * @param graphQLContext the graphql context in place + * @param locale the locale to use + * + * @return a parsed value which may be null + * + * @throws graphql.schema.CoercingParseLiteralException if input literal can't be parsed + */ + default @Nullable I parseLiteral(@NotNull Value input, @NotNull CoercedVariables variables, @NotNull GraphQLContext graphQLContext, @NotNull Locale locale) throws CoercingParseLiteralException { + assertNotNull(input); + assertNotNull(graphQLContext); + assertNotNull(locale); + return parseLiteral(input, variables.toMap()); + } + /** * Converts an external input value to a literal (Ast Value). - * + *

* IMPORTANT: the argument is validated before by calling {@link #parseValue(Object)}. * * @param input an external input value * * @return The literal matching the external input value. */ + @Deprecated + @DeprecatedAt("2022-08-22") default @NotNull Value valueToLiteral(@NotNull Object input) { throw new UnsupportedOperationException("This is not implemented by this Scalar " + this.getClass()); } + + /** + * Converts an external input value to a literal (Ast Value). + *

+ * IMPORTANT: the argument is validated before by calling {@link #parseValue(Object)}. + * + * @param input an external input value + * @param graphQLContext the graphql context in place + * @param locale the locale to use + * + * @return The literal matching the external input value. + */ + default @NotNull Value valueToLiteral(@NotNull Object input, @NotNull GraphQLContext graphQLContext, @NotNull Locale locale) { + assertNotNull(input); + assertNotNull(graphQLContext); + assertNotNull(locale); + return valueToLiteral(input); + } } diff --git a/src/main/java/graphql/schema/DataFetchingEnvironment.java b/src/main/java/graphql/schema/DataFetchingEnvironment.java index 153bee505f..578a234c36 100644 --- a/src/main/java/graphql/schema/DataFetchingEnvironment.java +++ b/src/main/java/graphql/schema/DataFetchingEnvironment.java @@ -1,5 +1,6 @@ package graphql.schema; +import graphql.DeprecatedAt; import graphql.GraphQLContext; import graphql.PublicApi; import graphql.cachecontrol.CacheControl; @@ -88,6 +89,7 @@ public interface DataFetchingEnvironment extends IntrospectionDataFetchingEnviro * @deprecated - use {@link #getGraphQlContext()} instead */ @Deprecated + @DeprecatedAt("2021-07-05") T getContext(); /** @@ -138,6 +140,7 @@ public interface DataFetchingEnvironment extends IntrospectionDataFetchingEnviro * @deprecated Use {@link #getMergedField()}. */ @Deprecated + @DeprecatedAt("2018-12-20") List getFields(); /** @@ -242,6 +245,7 @@ public interface DataFetchingEnvironment extends IntrospectionDataFetchingEnviro * @deprecated - Apollo has deprecated the Cache Control specification */ @Deprecated + @DeprecatedAt("2022-07-26") CacheControl getCacheControl(); /** diff --git a/src/main/java/graphql/schema/DataFetchingEnvironmentImpl.java b/src/main/java/graphql/schema/DataFetchingEnvironmentImpl.java index c5d7111ad6..b6fc50e92f 100644 --- a/src/main/java/graphql/schema/DataFetchingEnvironmentImpl.java +++ b/src/main/java/graphql/schema/DataFetchingEnvironmentImpl.java @@ -2,6 +2,7 @@ import com.google.common.collect.ImmutableMap; +import graphql.DeprecatedAt; import graphql.GraphQLContext; import graphql.Internal; import graphql.cachecontrol.CacheControl; @@ -219,6 +220,7 @@ public DataLoaderRegistry getDataLoaderRegistry() { @Override @Deprecated + @DeprecatedAt("2022-07-26") public CacheControl getCacheControl() { return cacheControl; } @@ -318,6 +320,7 @@ public Builder arguments(Supplier> arguments) { } @Deprecated + @DeprecatedAt("2021-07-05") public Builder context(Object context) { this.context = context; return this; @@ -393,6 +396,7 @@ public Builder dataLoaderRegistry(DataLoaderRegistry dataLoaderRegistry) { } @Deprecated + @DeprecatedAt("2022-07-26") public Builder cacheControl(CacheControl cacheControl) { this.cacheControl = cacheControl; return this; diff --git a/src/main/java/graphql/schema/DelegatingDataFetchingEnvironment.java b/src/main/java/graphql/schema/DelegatingDataFetchingEnvironment.java index dac7e6e480..c2da68c397 100644 --- a/src/main/java/graphql/schema/DelegatingDataFetchingEnvironment.java +++ b/src/main/java/graphql/schema/DelegatingDataFetchingEnvironment.java @@ -1,5 +1,6 @@ package graphql.schema; +import graphql.DeprecatedAt; import graphql.GraphQLContext; import graphql.PublicApi; import graphql.cachecontrol.CacheControl; @@ -65,6 +66,7 @@ public T getArgumentOrDefault(String name, T defaultValue) { } @Deprecated + @DeprecatedAt("2022-04-17") @Override public T getContext() { return delegateEnvironment.getContext(); @@ -91,6 +93,7 @@ public GraphQLFieldDefinition getFieldDefinition() { } @Deprecated + @DeprecatedAt("2019-10-07") @Override public List getFields() { return delegateEnvironment.getFields(); @@ -163,6 +166,7 @@ public Locale getLocale() { @Override @Deprecated + @DeprecatedAt("2022-07-26") public CacheControl getCacheControl() { return delegateEnvironment.getCacheControl(); } diff --git a/src/main/java/graphql/schema/GraphQLAppliedDirectiveArgument.java b/src/main/java/graphql/schema/GraphQLAppliedDirectiveArgument.java index dd595a5392..6f19bbd126 100644 --- a/src/main/java/graphql/schema/GraphQLAppliedDirectiveArgument.java +++ b/src/main/java/graphql/schema/GraphQLAppliedDirectiveArgument.java @@ -2,8 +2,8 @@ import graphql.Assert; +import graphql.GraphQLContext; import graphql.PublicApi; -import graphql.collect.ImmutableKit; import graphql.language.Argument; import graphql.language.Value; import graphql.util.TraversalControl; @@ -11,7 +11,9 @@ import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; +import java.util.ArrayList; import java.util.List; +import java.util.Locale; import java.util.function.Consumer; import static graphql.Assert.assertNotNull; @@ -24,7 +26,7 @@ * You can think of them as 'instances' of {@link GraphQLArgument}, when applied to a directive on a schema element */ @PublicApi -public class GraphQLAppliedDirectiveArgument implements GraphQLNamedSchemaElement { +public class GraphQLAppliedDirectiveArgument implements GraphQLNamedSchemaElement, GraphQLInputSchemaElement { private final String name; private final InputValueWithState value; @@ -34,6 +36,8 @@ public class GraphQLAppliedDirectiveArgument implements GraphQLNamedSchemaElemen private final Argument definition; + public static final String CHILD_TYPE = "type"; + private GraphQLAppliedDirectiveArgument(String name, InputValueWithState value, GraphQLInputType type, @@ -71,7 +75,7 @@ public boolean hasSetValue() { } /** - * This swill give out an internal java value based on the semantics captured + * This will give out an internal java value based on the semantics captured * in the {@link InputValueWithState} from {@link GraphQLAppliedDirectiveArgument#getArgumentValue()} * * Note : You MUST only call this on a {@link GraphQLAppliedDirectiveArgument} that is part of a fully formed schema. We need @@ -86,7 +90,7 @@ public boolean hasSetValue() { * @return a value of type T which is the java value of the argument */ public T getValue() { - return getInputValueImpl(getType(), value); + return getInputValueImpl(getType(), value, GraphQLContext.getDefault(), Locale.getDefault()); } /** @@ -104,19 +108,23 @@ public Argument getDefinition() { @Override public List getChildren() { - return ImmutableKit.emptyList(); + List children = new ArrayList<>(); + children.add(getType()); + return children; } - @Override public SchemaElementChildrenContainer getChildrenWithTypeReferences() { return SchemaElementChildrenContainer.newSchemaElementChildrenContainer() + .child(CHILD_TYPE, originalType) .build(); } @Override public GraphQLAppliedDirectiveArgument withNewChildren(SchemaElementChildrenContainer newChildren) { - return this; + return transform(builder -> + builder.type(newChildren.getChildOrNull(CHILD_TYPE)) + ); } @Override diff --git a/src/main/java/graphql/schema/GraphQLArgument.java b/src/main/java/graphql/schema/GraphQLArgument.java index d6aa2110d0..eb4f156ed8 100644 --- a/src/main/java/graphql/schema/GraphQLArgument.java +++ b/src/main/java/graphql/schema/GraphQLArgument.java @@ -1,7 +1,9 @@ package graphql.schema; +import graphql.DeprecatedAt; import graphql.DirectivesUtil; +import graphql.GraphQLContext; import graphql.PublicApi; import graphql.language.InputValueDefinition; import graphql.language.Value; @@ -12,6 +14,7 @@ import java.util.ArrayList; import java.util.List; +import java.util.Locale; import java.util.Map; import java.util.function.Consumer; @@ -123,6 +126,7 @@ public boolean hasSetValue() { * @deprecated use {@link GraphQLAppliedDirectiveArgument} instead */ @Deprecated + @DeprecatedAt("2022-02-24") public @NotNull InputValueWithState getArgumentValue() { return value; } @@ -146,8 +150,9 @@ public boolean hasSetValue() { * @deprecated use {@link GraphQLAppliedDirectiveArgument} instead */ @Deprecated + @DeprecatedAt("2022-02-24") public static T getArgumentValue(GraphQLArgument argument) { - return getInputValueImpl(argument.getType(), argument.getArgumentValue()); + return getInputValueImpl(argument.getType(), argument.getArgumentValue(), GraphQLContext.getDefault(), Locale.getDefault()); } /** @@ -167,7 +172,7 @@ public static T getArgumentValue(GraphQLArgument argument) { * @return a value of type T which is the java value of the argument default */ public static T getArgumentDefaultValue(GraphQLArgument argument) { - return getInputValueImpl(argument.getType(), argument.getArgumentDefaultValue()); + return getInputValueImpl(argument.getType(), argument.getArgumentDefaultValue(), GraphQLContext.getDefault(), Locale.getDefault()); } public String getDescription() { @@ -370,6 +375,7 @@ public Builder type(GraphQLInputType type) { * @deprecated use {@link #defaultValueLiteral(Value)} or {@link #defaultValueProgrammatic(Object)} */ @Deprecated + @DeprecatedAt("2021-05-10") public Builder defaultValue(Object defaultValue) { this.defaultValue = InputValueWithState.newInternalValue(defaultValue); return this; @@ -415,6 +421,7 @@ public Builder clearDefaultValue() { * @deprecated use {@link #valueLiteral(Value)} or {@link #valueProgrammatic(Object)} */ @Deprecated + @DeprecatedAt("2021-05-10") public Builder value(@Nullable Object value) { this.value = InputValueWithState.newInternalValue(value); return this; @@ -430,6 +437,7 @@ public Builder value(@Nullable Object value) { * @deprecated use {@link GraphQLAppliedDirectiveArgument} methods instead */ @Deprecated + @DeprecatedAt("2022-02-24") public Builder valueLiteral(@NotNull Value value) { this.value = InputValueWithState.newLiteralValue(value); return this; @@ -443,6 +451,7 @@ public Builder valueLiteral(@NotNull Value value) { * @deprecated use {@link GraphQLAppliedDirectiveArgument} methods instead */ @Deprecated + @DeprecatedAt("2022-02-24") public Builder valueProgrammatic(@Nullable Object value) { this.value = InputValueWithState.newExternalValue(value); return this; @@ -456,6 +465,7 @@ public Builder valueProgrammatic(@Nullable Object value) { * @deprecated use {@link GraphQLAppliedDirectiveArgument} methods instead */ @Deprecated + @DeprecatedAt("2022-02-24") public Builder clearValue() { this.value = InputValueWithState.NOT_SET; return this; diff --git a/src/main/java/graphql/schema/GraphQLDirectiveContainer.java b/src/main/java/graphql/schema/GraphQLDirectiveContainer.java index 518562e3f1..52caebef29 100644 --- a/src/main/java/graphql/schema/GraphQLDirectiveContainer.java +++ b/src/main/java/graphql/schema/GraphQLDirectiveContainer.java @@ -1,5 +1,6 @@ package graphql.schema; +import graphql.DeprecatedAt; import graphql.PublicApi; import java.util.List; @@ -8,15 +9,15 @@ import static graphql.collect.ImmutableKit.emptyList; /** - * Represents a graphql runtime type that can have {@link graphql.schema.GraphQLAppliedDirective}'s. + * Represents a graphql runtime type that can have {@link graphql.schema.GraphQLAppliedDirective}s. *

* Directives can be repeatable and (by default) non-repeatable. *

* There are access methods here that get the two different types. *

* The use of {@link GraphQLDirective} to represent a directive applied to an element is deprecated in favour of - * {@link GraphQLAppliedDirective}. A {@link GraphQLDirective} really should represent the definition of a directive in a schema not its use - * on schema elements. However, it has been left in place for legacy reasons and will be removed in a + * {@link GraphQLAppliedDirective}. A {@link GraphQLDirective} really should represent the definition of a directive in a schema, not its use + * on schema elements. However, it has been left in place for legacy reasons and will be removed in a * future version. * * @see graphql.language.DirectiveDefinition @@ -76,6 +77,7 @@ default List getAppliedDirectives(String directiveName) * @deprecated use {@link #hasAppliedDirective(String)} instead */ @Deprecated + @DeprecatedAt("2022-02-24") default boolean hasDirective(String directiveName) { return getAllDirectivesByName().containsKey(directiveName); } @@ -100,6 +102,7 @@ default boolean hasAppliedDirective(String directiveName) { * @deprecated - use the {@link GraphQLAppliedDirective} methods instead */ @Deprecated + @DeprecatedAt("2022-02-24") List getDirectives(); /** @@ -111,6 +114,7 @@ default boolean hasAppliedDirective(String directiveName) { * @deprecated - use the {@link GraphQLAppliedDirective} methods instead */ @Deprecated + @DeprecatedAt("2022-02-24") Map getDirectivesByName(); /** @@ -122,6 +126,7 @@ default boolean hasAppliedDirective(String directiveName) { * @deprecated - use the {@link GraphQLAppliedDirective} methods instead */ @Deprecated + @DeprecatedAt("2022-02-24") Map> getAllDirectivesByName(); /** @@ -135,6 +140,7 @@ default boolean hasAppliedDirective(String directiveName) { * @deprecated - use the {@link GraphQLAppliedDirective} methods instead */ @Deprecated + @DeprecatedAt("2022-02-24") GraphQLDirective getDirective(String directiveName); /** @@ -147,6 +153,7 @@ default boolean hasAppliedDirective(String directiveName) { * @deprecated - use the {@link GraphQLAppliedDirective} methods instead */ @Deprecated + @DeprecatedAt("2022-02-24") default List getDirectives(String directiveName) { return getAllDirectivesByName().getOrDefault(directiveName, emptyList()); } diff --git a/src/main/java/graphql/schema/GraphQLEnumType.java b/src/main/java/graphql/schema/GraphQLEnumType.java index 9dc553a080..27354e1e74 100644 --- a/src/main/java/graphql/schema/GraphQLEnumType.java +++ b/src/main/java/graphql/schema/GraphQLEnumType.java @@ -4,6 +4,7 @@ import com.google.common.collect.ImmutableList; import com.google.common.collect.ImmutableMap; import graphql.DirectivesUtil; +import graphql.GraphQLContext; import graphql.Internal; import graphql.PublicApi; import graphql.language.EnumTypeDefinition; @@ -14,9 +15,11 @@ import graphql.util.TraversalControl; import graphql.util.TraverserContext; +import javax.annotation.Nonnull; import java.util.ArrayList; import java.util.LinkedHashMap; import java.util.List; +import java.util.Locale; import java.util.Map; import java.util.function.Consumer; @@ -24,6 +27,8 @@ import static graphql.Assert.assertShouldNeverHappen; import static graphql.Assert.assertValidName; import static graphql.collect.ImmutableKit.emptyList; +import static graphql.scalar.CoercingUtil.i18nMsg; +import static graphql.scalar.CoercingUtil.typeName; import static graphql.schema.GraphQLEnumValueDefinition.newEnumValueDefinition; import static graphql.util.FpKit.getByName; @@ -67,43 +72,66 @@ private GraphQLEnumType(String name, } @Internal + @Deprecated public Object serialize(Object input) { - return getNameByValue(input); + return serialize(input, GraphQLContext.getDefault(), Locale.getDefault()); } @Internal + public Object serialize(Object input, GraphQLContext graphQLContext, Locale locale) { + return getNameByValue(input, graphQLContext, locale); + } + + @Internal + @Deprecated public Object parseValue(Object input) { - return getValueByName(input); + return getValueByName(input, GraphQLContext.getDefault(), Locale.getDefault()); } - private String typeName(Object input) { - if (input == null) { - return "null"; - } - return input.getClass().getSimpleName(); + @Internal + public Object parseValue(Object input, GraphQLContext graphQLContext, Locale locale) { + return getValueByName(input, graphQLContext, locale); } + @Internal + @Deprecated public Object parseLiteral(Object input) { + return parseLiteralImpl(input, GraphQLContext.getDefault(), Locale.getDefault()); + } + + @Internal + public Object parseLiteral(Value input, GraphQLContext graphQLContext, Locale locale) { + return parseLiteralImpl(input, graphQLContext, locale); + } + + private Object parseLiteralImpl(Object input, GraphQLContext graphQLContext, Locale locale) { if (!(input instanceof EnumValue)) { throw new CoercingParseLiteralException( - "Expected AST type 'EnumValue' but was '" + typeName(input) + "'." + i18nMsg(locale, "Scalar.unexpectedAstType", "EnumValue", typeName(input)) ); } EnumValue enumValue = (EnumValue) input; GraphQLEnumValueDefinition enumValueDefinition = valueDefinitionMap.get(enumValue.getName()); if (enumValueDefinition == null) { throw new CoercingParseLiteralException( - "Expected enum literal value not in allowable values - '" + input + "'." + i18nMsg(locale, "Enum.unallowableValue", getName(), input) ); } return enumValueDefinition.getValue(); } + @Internal + @Deprecated public Value valueToLiteral(Object input) { + return valueToLiteral(input, GraphQLContext.getDefault(), Locale.getDefault()); + } + + @Internal + public Value valueToLiteral(Object input, GraphQLContext graphQLContext, Locale locale) { GraphQLEnumValueDefinition enumValueDefinition = valueDefinitionMap.get(input.toString()); - assertNotNull(enumValueDefinition, () -> "Invalid input for Enum '" + name + "'. No value found for name '" + input + "'"); + assertNotNull(enumValueDefinition, () -> i18nMsg(locale, "Enum.badName", name, input.toString())); return EnumValue.newEnumValue(enumValueDefinition.getName()).build(); } @@ -121,15 +149,15 @@ private ImmutableMap buildMap(List assertShouldNeverHappen("Duplicated definition for field '%s' in type '%s'", fld1.getName(), this.name))); } - private Object getValueByName(Object value) { + private Object getValueByName(@Nonnull Object value, GraphQLContext graphQLContext, Locale locale) { GraphQLEnumValueDefinition enumValueDefinition = valueDefinitionMap.get(value.toString()); if (enumValueDefinition != null) { return enumValueDefinition.getValue(); } - throw new CoercingParseValueException("Invalid input for Enum '" + name + "'. No value found for name '" + value.toString() + "'"); + throw new CoercingParseValueException(i18nMsg(locale, "Enum.badName", name, value.toString())); } - private Object getNameByValue(Object value) { + private Object getNameByValue(Object value, GraphQLContext graphQLContext, Locale locale) { for (GraphQLEnumValueDefinition valueDefinition : valueDefinitionMap.values()) { Object definitionValue = valueDefinition.getValue(); if (value.equals(definitionValue)) { @@ -142,7 +170,7 @@ private Object getNameByValue(Object value) { } } } - // ok we didn't match on pure object.equals(). Lets try the Java enum strategy + // ok we didn't match on pure object.equals(). Let's try the Java enum strategy if (value instanceof Enum) { String enumNameValue = ((Enum) value).name(); for (GraphQLEnumValueDefinition valueDefinition : valueDefinitionMap.values()) { @@ -152,7 +180,7 @@ private Object getNameByValue(Object value) { } } } - throw new CoercingSerializeException("Invalid input for Enum '" + name + "'. Unknown value '" + value + "'"); + throw new CoercingSerializeException(i18nMsg(locale, "Enum.badInput", name, value)); } @Override diff --git a/src/main/java/graphql/schema/GraphQLFieldDefinition.java b/src/main/java/graphql/schema/GraphQLFieldDefinition.java index 2772a82715..865309e37f 100644 --- a/src/main/java/graphql/schema/GraphQLFieldDefinition.java +++ b/src/main/java/graphql/schema/GraphQLFieldDefinition.java @@ -2,6 +2,7 @@ import com.google.common.collect.ImmutableList; +import graphql.DeprecatedAt; import graphql.DirectivesUtil; import graphql.Internal; import graphql.PublicApi; @@ -86,6 +87,9 @@ public GraphQLOutputType getType() { } // to be removed in a future version when all code is in the code registry + @Internal + @Deprecated + @DeprecatedAt("2018-12-03") DataFetcher getDataFetcher() { if (dataFetcherFactory == null) { return null; @@ -307,6 +311,7 @@ public Builder type(GraphQLOutputType type) { * @deprecated use {@link graphql.schema.GraphQLCodeRegistry} instead */ @Deprecated + @DeprecatedAt("2018-12-03") public Builder dataFetcher(DataFetcher dataFetcher) { assertNotNull(dataFetcher, () -> "dataFetcher must be not null"); this.dataFetcherFactory = DataFetcherFactories.useDataFetcher(dataFetcher); @@ -323,6 +328,7 @@ public Builder dataFetcher(DataFetcher dataFetcher) { * @deprecated use {@link graphql.schema.GraphQLCodeRegistry} instead */ @Deprecated + @DeprecatedAt("2018-12-03") public Builder dataFetcherFactory(DataFetcherFactory dataFetcherFactory) { assertNotNull(dataFetcherFactory, () -> "dataFetcherFactory must be not null"); this.dataFetcherFactory = dataFetcherFactory; @@ -339,6 +345,7 @@ public Builder dataFetcherFactory(DataFetcherFactory dataFetcherFactory) { * @deprecated use {@link graphql.schema.GraphQLCodeRegistry} instead */ @Deprecated + @DeprecatedAt("2018-12-03") public Builder staticValue(final Object value) { this.dataFetcherFactory = DataFetcherFactories.useDataFetcher(environment -> value); return this; @@ -392,6 +399,7 @@ public Builder argument(GraphQLArgument.Builder builder) { * @deprecated This is a badly named method and is replaced by {@link #arguments(java.util.List)} */ @Deprecated + @DeprecatedAt("2019-02-06") public Builder argument(List arguments) { return arguments(arguments); } diff --git a/src/main/java/graphql/schema/GraphQLInputObjectField.java b/src/main/java/graphql/schema/GraphQLInputObjectField.java index ec2f31013b..0e88a907c1 100644 --- a/src/main/java/graphql/schema/GraphQLInputObjectField.java +++ b/src/main/java/graphql/schema/GraphQLInputObjectField.java @@ -1,7 +1,9 @@ package graphql.schema; +import graphql.DeprecatedAt; import graphql.DirectivesUtil; +import graphql.GraphQLContext; import graphql.PublicApi; import graphql.language.InputValueDefinition; import graphql.language.Value; @@ -11,6 +13,7 @@ import java.util.ArrayList; import java.util.List; +import java.util.Locale; import java.util.Map; import java.util.function.Consumer; @@ -106,7 +109,7 @@ public GraphQLInputType getType() { * @return a value of type T which is the java value of the input field default */ public static T getInputFieldDefaultValue(GraphQLInputObjectField inputObjectField) { - return getInputValueImpl(inputObjectField.getType(), inputObjectField.getInputFieldDefaultValue()); + return getInputValueImpl(inputObjectField.getType(), inputObjectField.getInputFieldDefaultValue(), GraphQLContext.getDefault(), Locale.getDefault()); } @@ -313,6 +316,7 @@ public Builder type(GraphQLInputType type) { * @deprecated use {@link #defaultValueLiteral(Value)} */ @Deprecated + @DeprecatedAt("2021-05-10") public Builder defaultValue(Object defaultValue) { this.defaultValue = InputValueWithState.newInternalValue(defaultValue); return this; diff --git a/src/main/java/graphql/schema/GraphQLInputSchemaElement.java b/src/main/java/graphql/schema/GraphQLInputSchemaElement.java new file mode 100644 index 0000000000..86b5b620f1 --- /dev/null +++ b/src/main/java/graphql/schema/GraphQLInputSchemaElement.java @@ -0,0 +1,10 @@ +package graphql.schema; + +import graphql.PublicApi; + +/** + * A schema element that is concerned with input. + */ +@PublicApi +public interface GraphQLInputSchemaElement extends GraphQLSchemaElement { +} diff --git a/src/main/java/graphql/schema/GraphQLInputType.java b/src/main/java/graphql/schema/GraphQLInputType.java index 4999cbe4fe..46fcf91301 100644 --- a/src/main/java/graphql/schema/GraphQLInputType.java +++ b/src/main/java/graphql/schema/GraphQLInputType.java @@ -8,5 +8,5 @@ * to {@link graphql.schema.GraphQLOutputType}s which can only be used as graphql response output. */ @PublicApi -public interface GraphQLInputType extends GraphQLType { +public interface GraphQLInputType extends GraphQLType, GraphQLInputSchemaElement { } diff --git a/src/main/java/graphql/schema/GraphQLInputValueDefinition.java b/src/main/java/graphql/schema/GraphQLInputValueDefinition.java index b0f3af62d4..bce4e31fa5 100644 --- a/src/main/java/graphql/schema/GraphQLInputValueDefinition.java +++ b/src/main/java/graphql/schema/GraphQLInputValueDefinition.java @@ -11,7 +11,7 @@ * @see graphql.schema.GraphQLArgument */ @PublicApi -public interface GraphQLInputValueDefinition extends GraphQLDirectiveContainer { +public interface GraphQLInputValueDefinition extends GraphQLDirectiveContainer, GraphQLInputSchemaElement { T getType(); } diff --git a/src/main/java/graphql/schema/GraphQLInterfaceType.java b/src/main/java/graphql/schema/GraphQLInterfaceType.java index 8f723cb48f..4f1cf1e51b 100644 --- a/src/main/java/graphql/schema/GraphQLInterfaceType.java +++ b/src/main/java/graphql/schema/GraphQLInterfaceType.java @@ -3,6 +3,7 @@ import com.google.common.collect.ImmutableList; import graphql.Assert; import graphql.AssertException; +import graphql.DeprecatedAt; import graphql.DirectivesUtil; import graphql.Internal; import graphql.PublicApi; @@ -112,6 +113,9 @@ public String getDescription() { } // to be removed in a future version when all code is in the code registry + @Internal + @Deprecated + @DeprecatedAt("2018-12-03") TypeResolver getTypeResolver() { return typeResolver; } @@ -359,8 +363,15 @@ public Builder clearFields() { return this; } - + /** + * @param typeResolver the type resolver + * + * @return this builder + * + * @deprecated use {@link graphql.schema.GraphQLCodeRegistry.Builder#typeResolver(GraphQLInterfaceType, TypeResolver)} instead + */ @Deprecated + @DeprecatedAt("2018-12-03") public Builder typeResolver(TypeResolver typeResolver) { this.typeResolver = typeResolver; return this; diff --git a/src/main/java/graphql/schema/GraphQLScalarType.java b/src/main/java/graphql/schema/GraphQLScalarType.java index b0443281e0..137a6047c0 100644 --- a/src/main/java/graphql/schema/GraphQLScalarType.java +++ b/src/main/java/graphql/schema/GraphQLScalarType.java @@ -28,7 +28,7 @@ * for example, a GraphQL system could define a scalar called Time which, while serialized as a string, promises to * conform to ISO‐8601. When querying a field of type Time, you can then rely on the ability to parse the result with an ISO‐8601 parser and use a client‐specific primitive for time. *

- * From the spec : http://facebook.github.io/graphql/#sec-Scalars + * From the spec : https://spec.graphql.org/October2021/#sec-Scalars * *

* graphql-java ships with a set of predefined scalar types via {@link graphql.Scalars} @@ -301,4 +301,4 @@ public GraphQLScalarType build() { specifiedByUrl); } } -} \ No newline at end of file +} diff --git a/src/main/java/graphql/schema/GraphQLSchema.java b/src/main/java/graphql/schema/GraphQLSchema.java index 31ea8c97ed..adbf61aaf1 100644 --- a/src/main/java/graphql/schema/GraphQLSchema.java +++ b/src/main/java/graphql/schema/GraphQLSchema.java @@ -5,6 +5,7 @@ import com.google.common.collect.ImmutableMap; import com.google.common.collect.ImmutableSet; import graphql.Assert; +import graphql.DeprecatedAt; import graphql.Directives; import graphql.DirectivesUtil; import graphql.Internal; @@ -418,6 +419,7 @@ public GraphQLObjectType getSubscriptionType() { * @deprecated use {@link GraphQLCodeRegistry#getFieldVisibility()} instead */ @Deprecated + @DeprecatedAt("2018-12-03") public GraphqlFieldVisibility getFieldVisibility() { return codeRegistry.getFieldVisibility(); } @@ -463,6 +465,7 @@ public GraphQLDirective getDirective(String directiveName) { * @deprecated Use the {@link GraphQLAppliedDirective} methods instead */ @Deprecated + @DeprecatedAt("2022-02-24") public List getSchemaDirectives() { return schemaAppliedDirectivesHolder.getDirectives(); } @@ -473,11 +476,12 @@ public List getSchemaDirectives() { * directives for all schema elements, whereas this is just for the schema * element itself * - * @return a map of directives + * @return a map of directives * * @deprecated Use the {@link GraphQLAppliedDirective} methods instead */ @Deprecated + @DeprecatedAt("2022-02-24") public Map getSchemaDirectiveByName() { return schemaAppliedDirectivesHolder.getDirectivesByName(); } @@ -493,6 +497,7 @@ public Map getSchemaDirectiveByName() { * @deprecated Use the {@link GraphQLAppliedDirective} methods instead */ @Deprecated + @DeprecatedAt("2022-02-24") public Map> getAllSchemaDirectivesByName() { return schemaAppliedDirectivesHolder.getAllDirectivesByName(); } @@ -510,6 +515,7 @@ public Map> getAllSchemaDirectivesByName() { * @deprecated Use the {@link GraphQLAppliedDirective} methods instead */ @Deprecated + @DeprecatedAt("2022-02-24") public GraphQLDirective getSchemaDirective(String directiveName) { return schemaAppliedDirectivesHolder.getDirective(directiveName); } @@ -525,6 +531,7 @@ public GraphQLDirective getSchemaDirective(String directiveName) { * @deprecated Use the {@link GraphQLAppliedDirective} methods instead */ @Deprecated + @DeprecatedAt("2022-02-24") public List getSchemaDirectives(String directiveName) { return schemaAppliedDirectivesHolder.getDirectives(directiveName); } @@ -736,6 +743,7 @@ public Builder subscription(GraphQLObjectType subscriptionType) { * @deprecated use {@link graphql.schema.GraphQLCodeRegistry.Builder#fieldVisibility(graphql.schema.visibility.GraphqlFieldVisibility)} instead */ @Deprecated + @DeprecatedAt("2018-12-03") public Builder fieldVisibility(GraphqlFieldVisibility fieldVisibility) { this.codeRegistry = this.codeRegistry.transform(builder -> builder.fieldVisibility(fieldVisibility)); return this; @@ -866,6 +874,7 @@ public Builder introspectionSchemaType(GraphQLObjectType introspectionSchemaType * @deprecated - Use the {@link #additionalType(GraphQLType)} methods */ @Deprecated + @DeprecatedAt("2018-07-30") public GraphQLSchema build(Set additionalTypes) { return additionalTypes(additionalTypes).build(); } @@ -881,6 +890,7 @@ public GraphQLSchema build(Set additionalTypes) { * @deprecated - Use the {@link #additionalType(GraphQLType)} and {@link #additionalDirective(GraphQLDirective)} methods */ @Deprecated + @DeprecatedAt("2018-07-30") public GraphQLSchema build(Set additionalTypes, Set additionalDirectives) { return additionalTypes(additionalTypes).additionalDirectives(additionalDirectives).build(); } diff --git a/src/main/java/graphql/schema/GraphQLUnionType.java b/src/main/java/graphql/schema/GraphQLUnionType.java index 33c6aad9be..f2813f9012 100644 --- a/src/main/java/graphql/schema/GraphQLUnionType.java +++ b/src/main/java/graphql/schema/GraphQLUnionType.java @@ -3,6 +3,7 @@ import com.google.common.collect.ImmutableList; import graphql.Assert; +import graphql.DeprecatedAt; import graphql.DirectivesUtil; import graphql.Internal; import graphql.PublicApi; @@ -100,6 +101,9 @@ public boolean isPossibleType(GraphQLObjectType graphQLObjectType) { } // to be removed in a future version when all code is in the code registry + @Internal + @Deprecated + @DeprecatedAt("2018-12-03") TypeResolver getTypeResolver() { return typeResolver; } @@ -271,7 +275,15 @@ public Builder extensionDefinitions(List extension return this; } + /** + * @param typeResolver the type resolver + * + * @return this builder + * + * @deprecated use {@link graphql.schema.GraphQLCodeRegistry.Builder#typeResolver(GraphQLUnionType, TypeResolver)} instead + */ @Deprecated + @DeprecatedAt("2018-12-03") public Builder typeResolver(TypeResolver typeResolver) { this.typeResolver = typeResolver; return this; diff --git a/src/main/java/graphql/schema/GraphqlDirectivesContainerTypeBuilder.java b/src/main/java/graphql/schema/GraphqlDirectivesContainerTypeBuilder.java index 4e3a513036..b42d2eb03a 100644 --- a/src/main/java/graphql/schema/GraphqlDirectivesContainerTypeBuilder.java +++ b/src/main/java/graphql/schema/GraphqlDirectivesContainerTypeBuilder.java @@ -1,5 +1,6 @@ package graphql.schema; +import graphql.DeprecatedAt; import graphql.Internal; import java.util.ArrayList; @@ -50,6 +51,7 @@ public B withAppliedDirective(GraphQLAppliedDirective.Builder builder) { * @deprecated - use the {@link GraphQLAppliedDirective} methods instead */ @Deprecated + @DeprecatedAt("2022-02-24") public B replaceDirectives(List directives) { assertNotNull(directives, () -> "directive can't be null"); this.directives.clear(); @@ -65,6 +67,7 @@ public B replaceDirectives(List directives) { * @deprecated - use the {@link GraphQLAppliedDirective} methods instead */ @Deprecated + @DeprecatedAt("2022-02-24") public B withDirectives(GraphQLDirective... directives) { assertNotNull(directives, () -> "directives can't be null"); this.directives.clear(); @@ -82,6 +85,7 @@ public B withDirectives(GraphQLDirective... directives) { * @deprecated - use the {@link GraphQLAppliedDirective} methods instead */ @Deprecated + @DeprecatedAt("2022-02-24") public B withDirective(GraphQLDirective directive) { assertNotNull(directive, () -> "directive can't be null"); this.directives.add(directive); @@ -96,6 +100,7 @@ public B withDirective(GraphQLDirective directive) { * @deprecated - use the {@link GraphQLAppliedDirective} methods instead */ @Deprecated + @DeprecatedAt("2022-02-24") public B withDirective(GraphQLDirective.Builder builder) { return withDirective(builder.build()); } diff --git a/src/main/java/graphql/schema/LightDataFetcher.java b/src/main/java/graphql/schema/LightDataFetcher.java new file mode 100644 index 0000000000..0458457c02 --- /dev/null +++ b/src/main/java/graphql/schema/LightDataFetcher.java @@ -0,0 +1,36 @@ +package graphql.schema; + +import graphql.TrivialDataFetcher; + +import java.util.function.Supplier; + +/** + * A {@link LightDataFetcher} is a specialised version of {@link DataFetcher} that is passed more lightweight arguments + * when it is asked to fetch values. The most common example of this is the {@link PropertyDataFetcher} which does not need + * all the {@link DataFetchingEnvironment} values to perform its duties. + * + * @param for two + */ +public interface LightDataFetcher extends TrivialDataFetcher { + + /** + * This is called to by the engine to get a value from the source object in a lightweight fashion. Only the field + * and source object are passed in a materialised way. The more heavy weight {@link DataFetchingEnvironment} is wrapped + * in a supplier that is only created on demand. + *

+ * If you are a lightweight data fetcher (like {@link PropertyDataFetcher} is) then you can implement this method to have a more lightweight + * method invocation. However, if you need field arguments etc. during fetching (most custom fetchers will) then you should use implement + * {@link #get(DataFetchingEnvironment)}. + * + * @param fieldDefinition the graphql field definition + * @param sourceObject the source object to get a value from + * @param environmentSupplier a supplier of the {@link DataFetchingEnvironment} that creates it lazily + * + * @return a value of type T. May be wrapped in a {@link graphql.execution.DataFetcherResult} + * + * @throws Exception to relieve the implementations from having to wrap checked exceptions. Any exception thrown + * from a {@code DataFetcher} will eventually be handled by the registered {@link graphql.execution.DataFetcherExceptionHandler} + * and the related field will have a value of {@code null} in the result. + */ + T get(GraphQLFieldDefinition fieldDefinition, Object sourceObject, Supplier environmentSupplier) throws Exception; +} diff --git a/src/main/java/graphql/schema/PropertyDataFetcher.java b/src/main/java/graphql/schema/PropertyDataFetcher.java index f228f19863..77cfdcf062 100644 --- a/src/main/java/graphql/schema/PropertyDataFetcher.java +++ b/src/main/java/graphql/schema/PropertyDataFetcher.java @@ -3,25 +3,25 @@ import graphql.Assert; import graphql.PublicApi; -import graphql.TrivialDataFetcher; import java.util.function.Function; +import java.util.function.Supplier; /** - * This is the default data fetcher used in graphql-java. It will examine - * maps and POJO java beans for values that match the desired name, typically the field name + * This is the default data fetcher used in graphql-java, and it will examine + * maps, records and POJO java beans for values that match the desired name, typically the field name, * or it will use a provided function to obtain values. - * maps and POJO java beans for values that match the desired name. *

* It uses the following strategies *

    *
  • If the source is null, return null
  • *
  • If the source is a Map, return map.get(propertyName)
  • *
  • If a function is provided, it is used
  • - *
  • Find a public JavaBean getter method named `propertyName`
  • - *
  • Find any getter method named `propertyName` and call method.setAccessible(true)
  • + *
  • Find a public JavaBean getter method named `getPropertyName()` or `isPropertyName()`
  • + *
  • Find any getter method named `getPropertyName()` or `isPropertyName()` and call method.setAccessible(true)
  • *
  • Find a public field named `propertyName`
  • *
  • Find any field named `propertyName` and call field.setAccessible(true)
  • + *
  • Find a public Record like method named `propertyName()`
  • *
  • If this cant find anything, then null is returned
  • *
*

@@ -31,7 +31,7 @@ * @see graphql.schema.DataFetcher */ @PublicApi -public class PropertyDataFetcher implements DataFetcher, TrivialDataFetcher { +public class PropertyDataFetcher implements LightDataFetcher { private final String propertyName; private final Function function; @@ -69,6 +69,7 @@ private PropertyDataFetcher(Function function) { * * @param propertyName the name of the property to retrieve * @param the type of result + * * @return a new PropertyDataFetcher using the provided function as its source of values */ public static PropertyDataFetcher fetching(String propertyName) { @@ -92,6 +93,7 @@ public static PropertyDataFetcher fetching(String propertyName) { * @param function the function to use to obtain a value from the source object * @param the type of the source object * @param the type of result + * * @return a new PropertyDataFetcher using the provided function as its source of values */ public static PropertyDataFetcher fetching(Function function) { @@ -105,10 +107,19 @@ public String getPropertyName() { return propertyName; } - @SuppressWarnings("unchecked") + @Override + public T get(GraphQLFieldDefinition fieldDefinition, Object source, Supplier environmentSupplier) throws Exception { + return getImpl(source, fieldDefinition.getType(), environmentSupplier); + } + @Override public T get(DataFetchingEnvironment environment) { Object source = environment.getSource(); + return getImpl(source, environment.getFieldType(), () -> environment); + } + + @SuppressWarnings("unchecked") + private T getImpl(Object source, GraphQLOutputType fieldDefinition, Supplier environmentSupplier) { if (source == null) { return null; } @@ -117,7 +128,7 @@ public T get(DataFetchingEnvironment environment) { return (T) function.apply(source); } - return (T) PropertyDataFetcherHelper.getPropertyValue(propertyName, source, environment.getFieldType(), environment); + return (T) PropertyDataFetcherHelper.getPropertyValue(propertyName, source, fieldDefinition, environmentSupplier); } /** @@ -138,6 +149,7 @@ public static void clearReflectionCache() { * values. By default it PropertyDataFetcher WILL use setAccessible. * * @param flag whether to use setAccessible + * * @return the previous value of the flag */ public static boolean setUseSetAccessible(boolean flag) { @@ -148,6 +160,7 @@ public static boolean setUseSetAccessible(boolean flag) { * This can be used to control whether PropertyDataFetcher will cache negative lookups for a property for performance reasons. By default it PropertyDataFetcher WILL cache misses. * * @param flag whether to cache misses + * * @return the previous value of the flag */ public static boolean setUseNegativeCache(boolean flag) { diff --git a/src/main/java/graphql/schema/PropertyDataFetcherHelper.java b/src/main/java/graphql/schema/PropertyDataFetcherHelper.java index a7223763f1..2c38b5e127 100644 --- a/src/main/java/graphql/schema/PropertyDataFetcherHelper.java +++ b/src/main/java/graphql/schema/PropertyDataFetcherHelper.java @@ -1,6 +1,9 @@ package graphql.schema; import graphql.Internal; +import graphql.VisibleForTesting; + +import java.util.function.Supplier; /** * This class is the guts of a property data fetcher and also used in AST code to turn @@ -12,11 +15,11 @@ public class PropertyDataFetcherHelper { private static final PropertyFetchingImpl impl = new PropertyFetchingImpl(DataFetchingEnvironment.class); public static Object getPropertyValue(String propertyName, Object object, GraphQLType graphQLType) { - return impl.getPropertyValue(propertyName, object, graphQLType, null); + return impl.getPropertyValue(propertyName, object, graphQLType, false, () -> null); } - public static Object getPropertyValue(String propertyName, Object object, GraphQLType graphQLType, DataFetchingEnvironment environment) { - return impl.getPropertyValue(propertyName, object, graphQLType, environment); + public static Object getPropertyValue(String propertyName, Object object, GraphQLType graphQLType, Supplier environment) { + return impl.getPropertyValue(propertyName, object, graphQLType, true, environment::get); } public static void clearReflectionCache() { @@ -27,6 +30,11 @@ public static boolean setUseSetAccessible(boolean flag) { return impl.setUseSetAccessible(flag); } + @VisibleForTesting + public static boolean setUseLambdaFactory(boolean flag) { + return impl.setUseLambdaFactory(flag); + } + public static boolean setUseNegativeCache(boolean flag) { return impl.setUseNegativeCache(flag); } diff --git a/src/main/java/graphql/schema/PropertyFetchingImpl.java b/src/main/java/graphql/schema/PropertyFetchingImpl.java index a671388d3b..75a795a0ee 100644 --- a/src/main/java/graphql/schema/PropertyFetchingImpl.java +++ b/src/main/java/graphql/schema/PropertyFetchingImpl.java @@ -2,6 +2,7 @@ import graphql.GraphQLException; import graphql.Internal; +import graphql.schema.fetching.LambdaFetchingSupport; import java.lang.reflect.Field; import java.lang.reflect.InvocationTargetException; @@ -15,7 +16,9 @@ import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ConcurrentMap; import java.util.concurrent.atomic.AtomicBoolean; +import java.util.function.Function; import java.util.function.Predicate; +import java.util.function.Supplier; import static graphql.Assert.assertShouldNeverHappen; import static graphql.Scalars.GraphQLBoolean; @@ -29,7 +32,9 @@ public class PropertyFetchingImpl { private final AtomicBoolean USE_SET_ACCESSIBLE = new AtomicBoolean(true); + private final AtomicBoolean USE_LAMBDA_FACTORY = new AtomicBoolean(true); private final AtomicBoolean USE_NEGATIVE_CACHE = new AtomicBoolean(true); + private final ConcurrentMap LAMBDA_CACHE = new ConcurrentHashMap<>(); private final ConcurrentMap METHOD_CACHE = new ConcurrentHashMap<>(); private final ConcurrentMap FIELD_CACHE = new ConcurrentHashMap<>(); private final ConcurrentMap NEGATIVE_CACHE = new ConcurrentHashMap<>(); @@ -39,25 +44,37 @@ public PropertyFetchingImpl(Class singleArgumentType) { this.singleArgumentType = singleArgumentType; } - private class CachedMethod { - Method method; - boolean takesSingleArgumentTypeAsOnlyArgument; + private final class CachedMethod { + private final Method method; + private final boolean takesSingleArgumentTypeAsOnlyArgument; CachedMethod(Method method) { this.method = method; this.takesSingleArgumentTypeAsOnlyArgument = takesSingleArgumentTypeAsOnlyArgument(method); } + } + + private static final class CachedLambdaFunction { + private final Function getter; + CachedLambdaFunction(Function getter) { + this.getter = getter; + } } - public Object getPropertyValue(String propertyName, Object object, GraphQLType graphQLType, Object singleArgumentValue) { + public Object getPropertyValue(String propertyName, Object object, GraphQLType graphQLType, boolean dfeInUse, Supplier singleArgumentValue) { if (object instanceof Map) { return ((Map) object).get(propertyName); } CacheKey cacheKey = mkCacheKey(object, propertyName); - // lets try positive cache mechanisms first. If we have seen the method or field before + + // let's try positive cache mechanisms first. If we have seen the method or field before // then we invoke it directly without burning any cycles doing reflection. + CachedLambdaFunction cachedFunction = LAMBDA_CACHE.get(cacheKey); + if (cachedFunction != null) { + return cachedFunction.getter.apply(object); + } CachedMethod cachedMethod = METHOD_CACHE.get(cacheKey); if (cachedMethod != null) { try { @@ -72,9 +89,9 @@ public Object getPropertyValue(String propertyName, Object object, GraphQLType g } // - // if we have tried all strategies before and they have all failed then we negatively cache + // if we have tried all strategies before, and they have all failed then we negatively cache // the cacheKey and assume that it's never going to turn up. This shortcuts the property lookup - // in systems where there was a `foo` graphql property but they never provided an POJO + // in systems where there was a `foo` graphql property, but they never provided an POJO // version of `foo`. // // we do this second because we believe in the positive cached version will mostly prevail @@ -83,28 +100,57 @@ public Object getPropertyValue(String propertyName, Object object, GraphQLType g if (isNegativelyCached(cacheKey)) { return null; } + // - // ok we haven't cached it and we haven't negatively cached it so we have to find the POJO method which is the most + // ok we haven't cached it, and we haven't negatively cached it, so we have to find the POJO method which is the most // expensive operation here // - boolean dfeInUse = singleArgumentValue != null; + + Optional> getterOpt = lambdaGetter(propertyName, object); + if (getterOpt.isPresent()) { + Function getter = getterOpt.get(); + cachedFunction = new CachedLambdaFunction(getter); + LAMBDA_CACHE.putIfAbsent(cacheKey, cachedFunction); + return getter.apply(object); + } + + // + // try by record like name - object.propertyName() try { - MethodFinder methodFinder = (root, methodName) -> findPubliclyAccessibleMethod(cacheKey, root, methodName, dfeInUse); + MethodFinder methodFinder = (rootClass, methodName) -> findRecordMethod(cacheKey, rootClass, methodName); + return getPropertyViaRecordMethod(object, propertyName, methodFinder, singleArgumentValue); + } catch (NoSuchMethodException ignored) { + } + // + // try by public getters name - object.getPropertyName() + try { + MethodFinder methodFinder = (rootClass, methodName) -> findPubliclyAccessibleMethod(cacheKey, rootClass, methodName, dfeInUse); return getPropertyViaGetterMethod(object, propertyName, graphQLType, methodFinder, singleArgumentValue); } catch (NoSuchMethodException ignored) { - try { - MethodFinder methodFinder = (aClass, methodName) -> findViaSetAccessible(cacheKey, aClass, methodName, dfeInUse); - return getPropertyViaGetterMethod(object, propertyName, graphQLType, methodFinder, singleArgumentValue); - } catch (NoSuchMethodException ignored2) { - try { - return getPropertyViaFieldAccess(cacheKey, object, propertyName); - } catch (FastNoSuchMethodException e) { - // we have nothing to ask for and we have exhausted our lookup strategies - putInNegativeCache(cacheKey); - return null; - } - } } + // + // try by accessible getters name - object.getPropertyName() + try { + MethodFinder methodFinder = (aClass, methodName) -> findViaSetAccessible(cacheKey, aClass, methodName, dfeInUse); + return getPropertyViaGetterMethod(object, propertyName, graphQLType, methodFinder, singleArgumentValue); + } catch (NoSuchMethodException ignored) { + } + // + // try by field name - object.propertyName; + try { + return getPropertyViaFieldAccess(cacheKey, object, propertyName); + } catch (NoSuchMethodException ignored) { + } + // we have nothing to ask for, and we have exhausted our lookup strategies + putInNegativeCache(cacheKey); + return null; + } + + private Optional> lambdaGetter(String propertyName, Object object) { + if (USE_LAMBDA_FACTORY.get()) { + return LambdaFetchingSupport.createGetter(object.getClass(), propertyName); + } + return Optional.empty(); } private boolean isNegativelyCached(CacheKey key) { @@ -124,7 +170,12 @@ private interface MethodFinder { Method apply(Class aClass, String s) throws NoSuchMethodException; } - private Object getPropertyViaGetterMethod(Object object, String propertyName, GraphQLType graphQLType, MethodFinder methodFinder, Object singleArgumentValue) throws NoSuchMethodException { + private Object getPropertyViaRecordMethod(Object object, String propertyName, MethodFinder methodFinder, Supplier singleArgumentValue) throws NoSuchMethodException { + Method method = methodFinder.apply(object.getClass(), propertyName); + return invokeMethod(object, singleArgumentValue, method, takesSingleArgumentTypeAsOnlyArgument(method)); + } + + private Object getPropertyViaGetterMethod(Object object, String propertyName, GraphQLType graphQLType, MethodFinder methodFinder, Supplier singleArgumentValue) throws NoSuchMethodException { if (isBooleanProperty(graphQLType)) { try { return getPropertyViaGetterUsingPrefix(object, propertyName, "is", methodFinder, singleArgumentValue); @@ -136,7 +187,7 @@ private Object getPropertyViaGetterMethod(Object object, String propertyName, Gr } } - private Object getPropertyViaGetterUsingPrefix(Object object, String propertyName, String prefix, MethodFinder methodFinder, Object singleArgumentValue) throws NoSuchMethodException { + private Object getPropertyViaGetterUsingPrefix(Object object, String propertyName, String prefix, MethodFinder methodFinder, Supplier singleArgumentValue) throws NoSuchMethodException { String getterName = prefix + propertyName.substring(0, 1).toUpperCase() + propertyName.substring(1); Method method = methodFinder.apply(object.getClass(), getterName); return invokeMethod(object, singleArgumentValue, method, takesSingleArgumentTypeAsOnlyArgument(method)); @@ -179,6 +230,20 @@ private Method findPubliclyAccessibleMethod(CacheKey cacheKey, Class rootClas return rootClass.getMethod(methodName); } + /* + https://docs.oracle.com/en/java/javase/15/language/records.html + + A record class declares a sequence of fields, and then the appropriate accessors, constructors, equals, hashCode, and toString methods are created automatically. + + Records cannot extend any class - so we need only check the root class for a publicly declared method with the propertyName + + However, we won't just restrict ourselves strictly to true records. We will find methods that are record like + and fetch them - e.g. `object.propertyName()` + */ + private Method findRecordMethod(CacheKey cacheKey, Class rootClass, String methodName) throws NoSuchMethodException { + return findPubliclyAccessibleMethod(cacheKey,rootClass,methodName,false); + } + private Method findViaSetAccessible(CacheKey cacheKey, Class aClass, String methodName, boolean dfeInUse) throws NoSuchMethodException { if (!USE_SET_ACCESSIBLE.get()) { throw new FastNoSuchMethodException(methodName); @@ -237,13 +302,14 @@ private Object getPropertyViaFieldAccess(CacheKey cacheKey, Object object, Strin } } - private Object invokeMethod(Object object, Object singleArgumentValue, Method method, boolean takesSingleArgument) throws FastNoSuchMethodException { + private Object invokeMethod(Object object, Supplier singleArgumentValue, Method method, boolean takesSingleArgument) throws FastNoSuchMethodException { try { if (takesSingleArgument) { - if (singleArgumentValue == null) { + Object argValue = singleArgumentValue.get(); + if (argValue == null) { throw new FastNoSuchMethodException(method.getName()); } - return method.invoke(object, singleArgumentValue); + return method.invoke(object, argValue); } else { return method.invoke(object); } @@ -272,6 +338,7 @@ private boolean isBooleanProperty(GraphQLType graphQLType) { } public void clearReflectionCache() { + LAMBDA_CACHE.clear(); METHOD_CACHE.clear(); FIELD_CACHE.clear(); NEGATIVE_CACHE.clear(); @@ -280,6 +347,9 @@ public void clearReflectionCache() { public boolean setUseSetAccessible(boolean flag) { return USE_SET_ACCESSIBLE.getAndSet(flag); } + public boolean setUseLambdaFactory(boolean flag) { + return USE_LAMBDA_FACTORY.getAndSet(flag); + } public boolean setUseNegativeCache(boolean flag) { return USE_NEGATIVE_CACHE.getAndSet(flag); @@ -304,8 +374,12 @@ private CacheKey(ClassLoader classLoader, String className, String propertyName) @Override public boolean equals(Object o) { - if (this == o) return true; - if (!(o instanceof CacheKey)) return false; + if (this == o) { + return true; + } + if (!(o instanceof CacheKey)) { + return false; + } CacheKey cacheKey = (CacheKey) o; return Objects.equals(classLoader, cacheKey.classLoader) && Objects.equals(className, cacheKey.className) && Objects.equals(propertyName, cacheKey.propertyName); } @@ -322,10 +396,10 @@ public int hashCode() { @Override public String toString() { return "CacheKey{" + - "classLoader=" + classLoader + - ", className='" + className + '\'' + - ", propertyName='" + propertyName + '\'' + - '}'; + "classLoader=" + classLoader + + ", className='" + className + '\'' + + ", propertyName='" + propertyName + '\'' + + '}'; } } @@ -343,7 +417,6 @@ private static Comparator mostMethodArgsFirst() { return Comparator.comparingInt(Method::getParameterCount).reversed(); } - @SuppressWarnings("serial") private static class FastNoSuchMethodException extends NoSuchMethodException { public FastNoSuchMethodException(String methodName) { super(methodName); diff --git a/src/main/java/graphql/schema/diff/DiffCtx.java b/src/main/java/graphql/schema/diff/DiffCtx.java index 048685d0c3..975189c174 100644 --- a/src/main/java/graphql/schema/diff/DiffCtx.java +++ b/src/main/java/graphql/schema/diff/DiffCtx.java @@ -11,7 +11,6 @@ import java.util.Deque; import java.util.List; import java.util.Optional; -import java.util.Stack; /* * A helper class that represents diff state (eg visited types) as well as helpers diff --git a/src/main/java/graphql/schema/diff/DiffEvent.java b/src/main/java/graphql/schema/diff/DiffEvent.java index e11088b4bf..11d07a60be 100644 --- a/src/main/java/graphql/schema/diff/DiffEvent.java +++ b/src/main/java/graphql/schema/diff/DiffEvent.java @@ -1,5 +1,6 @@ package graphql.schema.diff; +import graphql.DeprecatedAt; import graphql.PublicApi; import graphql.language.TypeKind; @@ -78,6 +79,7 @@ public String toString() { * @deprecated use {@link DiffEvent#apiInfo()} instead */ @Deprecated + @DeprecatedAt("2017-12-27") public static Builder newInfo() { return new Builder().level(DiffLevel.INFO); } diff --git a/src/main/java/graphql/schema/diff/SchemaDiff.java b/src/main/java/graphql/schema/diff/SchemaDiff.java index 52c2fa0747..18e34967f9 100644 --- a/src/main/java/graphql/schema/diff/SchemaDiff.java +++ b/src/main/java/graphql/schema/diff/SchemaDiff.java @@ -70,7 +70,7 @@ public static Options defaultOptions() { private static class CountingReporter implements DifferenceReporter { final DifferenceReporter delegate; - int breakingCount = 1; + int breakingCount = 0; private CountingReporter(DifferenceReporter delegate) { this.delegate = delegate; @@ -397,7 +397,7 @@ private void checkInputFields(DiffCtx ctx, TypeDefinition old, List oldDirecti } } - DiffCategory checkTypeWithNonNullAndList(Type oldType, Type newType) { + DiffCategory checkTypeWithNonNullAndListOnInputOrArg(Type oldType, Type newType) { TypeInfo oldTypeInfo = typeInfo(oldType); TypeInfo newTypeInfo = typeInfo(newType); @@ -840,31 +840,83 @@ DiffCategory checkTypeWithNonNullAndList(Type oldType, Type newType) { } while (true) { - // - // its allowed to get more less strict in the new but not more strict - if (oldTypeInfo.isNonNull() && newTypeInfo.isNonNull()) { - oldTypeInfo = oldTypeInfo.unwrapOne(); - newTypeInfo = newTypeInfo.unwrapOne(); - } else if (oldTypeInfo.isNonNull() && !newTypeInfo.isNonNull()) { - oldTypeInfo = oldTypeInfo.unwrapOne(); - } else if (!oldTypeInfo.isNonNull() && newTypeInfo.isNonNull()) { - return DiffCategory.STRICTER; - } - // lists - if (oldTypeInfo.isList() && !newTypeInfo.isList()) { - return DiffCategory.INVALID; + if (oldTypeInfo.isNonNull()) { + if (newTypeInfo.isNonNull()) { + // if they're both non-null, compare the unwrapped types + oldTypeInfo = oldTypeInfo.unwrapOne(); + newTypeInfo = newTypeInfo.unwrapOne(); + } else { + // non-null to nullable is valid, as long as the underlying types are also valid + oldTypeInfo = oldTypeInfo.unwrapOne(); + } + } else if (oldTypeInfo.isList()) { + if (newTypeInfo.isList()) { + // if they're both lists, compare the unwrapped types + oldTypeInfo = oldTypeInfo.unwrapOne(); + newTypeInfo = newTypeInfo.unwrapOne(); + } else if (newTypeInfo.isNonNull()) { + // nullable to non-null creates a stricter input requirement for clients to specify + return DiffCategory.STRICTER; + } else { + // list to non-list is not valid + return DiffCategory.INVALID; + } + } else { + if (newTypeInfo.isNonNull()) { + // nullable to non-null creates a stricter input requirement for clients to specify + return DiffCategory.STRICTER; + } else if (newTypeInfo.isList()) { + // non-list to list is not valid + return DiffCategory.INVALID; + } else { + return null; + } } - // plain - if (oldTypeInfo.isPlain()) { - if (!newTypeInfo.isPlain()) { + } + } + + DiffCategory checkTypeWithNonNullAndListOnObjectOrInterface(Type oldType, Type newType) { + TypeInfo oldTypeInfo = typeInfo(oldType); + TypeInfo newTypeInfo = typeInfo(newType); + + if (!oldTypeInfo.getName().equals(newTypeInfo.getName())) { + return DiffCategory.INVALID; + } + + while (true) { + if (oldTypeInfo.isNonNull()) { + if (newTypeInfo.isNonNull()) { + // if they're both non-null, compare the unwrapped types + oldTypeInfo = oldTypeInfo.unwrapOne(); + newTypeInfo = newTypeInfo.unwrapOne(); + } else { + // non-null to nullable requires a stricter check from clients since it removes the guarantee of presence + return DiffCategory.STRICTER; + } + } else if (oldTypeInfo.isList()) { + if (newTypeInfo.isList()) { + // if they're both lists, compare the unwrapped types + oldTypeInfo = oldTypeInfo.unwrapOne(); + newTypeInfo = newTypeInfo.unwrapOne(); + } else if (newTypeInfo.isNonNull()) { + // nullable to non-null is valid, as long as the underlying types are also valid + newTypeInfo = newTypeInfo.unwrapOne(); + } else { + // list to non-list is not valid return DiffCategory.INVALID; } - break; + } else { + if (newTypeInfo.isNonNull()) { + // nullable to non-null is valid, as long as the underlying types are also valid + newTypeInfo = newTypeInfo.unwrapOne(); + } else if (newTypeInfo.isList()) { + // non-list to list is not valid + return DiffCategory.INVALID; + } else { + return null; + } } - oldTypeInfo = oldTypeInfo.unwrapOne(); - newTypeInfo = newTypeInfo.unwrapOne(); } - return null; } diff --git a/src/main/java/graphql/schema/diffing/DiffImpl.java b/src/main/java/graphql/schema/diffing/DiffImpl.java new file mode 100644 index 0000000000..ee8c847f0d --- /dev/null +++ b/src/main/java/graphql/schema/diffing/DiffImpl.java @@ -0,0 +1,413 @@ +package graphql.schema.diffing; + +import com.google.common.collect.HashMultiset; +import com.google.common.collect.Multiset; +import com.google.common.collect.Multisets; +import com.google.common.util.concurrent.AtomicDoubleArray; +import graphql.Internal; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.LinkedHashSet; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.PriorityQueue; +import java.util.Set; +import java.util.concurrent.LinkedBlockingQueue; + +import static graphql.Assert.assertTrue; +import static graphql.schema.diffing.EditorialCostForMapping.editorialCostForMapping; + +@Internal +public class DiffImpl { + + private static MappingEntry LAST_ELEMENT = new MappingEntry(); + private SchemaGraph completeSourceGraph; + private SchemaGraph completeTargetGraph; + private FillupIsolatedVertices.IsolatedVertices isolatedVertices; + + private static class MappingEntry { + public boolean siblingsFinished; + public LinkedBlockingQueue mappingEntriesSiblings; + public int[] assignments; + public List availableTargetVertices; + + Mapping partialMapping = new Mapping(); + int level; // = partialMapping.size + double lowerBoundCost; + + public MappingEntry(Mapping partialMapping, int level, double lowerBoundCost) { + this.partialMapping = partialMapping; + this.level = level; + this.lowerBoundCost = lowerBoundCost; + } + + public MappingEntry() { + + } + } + + public static class OptimalEdit { + public List mappings = new ArrayList<>(); + public List> listOfEditOperations = new ArrayList<>(); + + public List> listOfSets = new ArrayList<>(); + + public int ged = Integer.MAX_VALUE; + + public OptimalEdit() { + + } + + public OptimalEdit(List mappings, List> listOfEditOperations, int ged) { + this.mappings = mappings; + this.listOfEditOperations = listOfEditOperations; + this.ged = ged; + } + } + + public DiffImpl(SchemaGraph completeSourceGraph, SchemaGraph completeTargetGraph, FillupIsolatedVertices.IsolatedVertices isolatedVertices) { + this.completeSourceGraph = completeSourceGraph; + this.completeTargetGraph = completeTargetGraph; + this.isolatedVertices = isolatedVertices; + } + + OptimalEdit diffImpl(Mapping startMapping, List relevantSourceList, List relevantTargetList) throws Exception { + + int graphSize = relevantSourceList.size(); + + ArrayList initialEditOperations = new ArrayList<>(); + int mappingCost = editorialCostForMapping(startMapping, completeSourceGraph, completeTargetGraph, initialEditOperations); + int level = startMapping.size(); + MappingEntry firstMappingEntry = new MappingEntry(startMapping, level, mappingCost); + System.out.println("first entry: lower bound: " + mappingCost + " at level " + level); + + OptimalEdit optimalEdit = new OptimalEdit(); + PriorityQueue queue = new PriorityQueue<>((mappingEntry1, mappingEntry2) -> { + int compareResult = Double.compare(mappingEntry1.lowerBoundCost, mappingEntry2.lowerBoundCost); + if (compareResult == 0) { + return Integer.compare(mappingEntry2.level, mappingEntry1.level); + } else { + return compareResult; + } + }); + queue.add(firstMappingEntry); + firstMappingEntry.siblingsFinished = true; +// queue.add(new MappingEntry()); +// int counter = 0; + while (!queue.isEmpty()) { + MappingEntry mappingEntry = queue.poll(); +// System.out.println((++counter) + " check entry at level " + mappingEntry.level + " queue size: " + queue.size() + " lower bound " + mappingEntry.lowerBoundCost + " map " + getDebugMap(mappingEntry.partialMapping)); +// if ((++counter) % 100 == 0) { +// System.out.println((counter) + " entry at level"); +// } + if (mappingEntry.lowerBoundCost >= optimalEdit.ged) { + continue; + } + if (mappingEntry.level > 0 && !mappingEntry.siblingsFinished) { + addSiblingToQueue( + mappingEntry.level, + queue, + optimalEdit, + relevantSourceList, + relevantTargetList, + mappingEntry); + } + if (mappingEntry.level < graphSize) { + addChildToQueue(mappingEntry, + queue, + optimalEdit, + relevantSourceList, + relevantTargetList + ); + } + } + return optimalEdit; + } + + + // this calculates all children for the provided parentEntry, but only the first is directly added to the queue + private void addChildToQueue(MappingEntry parentEntry, + PriorityQueue queue, + OptimalEdit optimalEdit, + List sourceList, + List targetList + + ) { + Mapping partialMapping = parentEntry.partialMapping; + int level = parentEntry.level; + + assertTrue(level == partialMapping.size()); + + ArrayList availableTargetVertices = new ArrayList<>(targetList); + availableTargetVertices.removeAll(partialMapping.getTargets()); + assertTrue(availableTargetVertices.size() + partialMapping.size() == targetList.size()); + Vertex v_i = sourceList.get(level); + + // the cost matrix is for the non mapped vertices + int costMatrixSize = sourceList.size() - level; + + // costMatrix gets modified by the hungarian algorithm ... therefore we create two of them + AtomicDoubleArray[] costMatrixForHungarianAlgo = new AtomicDoubleArray[costMatrixSize]; + Arrays.setAll(costMatrixForHungarianAlgo, (index) -> new AtomicDoubleArray(costMatrixSize)); + AtomicDoubleArray[] costMatrix = new AtomicDoubleArray[costMatrixSize]; + Arrays.setAll(costMatrix, (index) -> new AtomicDoubleArray(costMatrixSize)); + + // we are skipping the first level -i indices + Set partialMappingSourceSet = new LinkedHashSet<>(partialMapping.getSources()); + Set partialMappingTargetSet = new LinkedHashSet<>(partialMapping.getTargets()); + + + for (int i = level; i < sourceList.size(); i++) { + Vertex v = sourceList.get(i); + int j = 0; + for (Vertex u : availableTargetVertices) { + double cost = calcLowerBoundMappingCost(v, u, partialMapping.getSources(), partialMappingSourceSet, partialMapping.getTargets(), partialMappingTargetSet); + costMatrixForHungarianAlgo[i - level].set(j, cost); + costMatrix[i - level].set(j, cost); + j++; + } + } + HungarianAlgorithm hungarianAlgorithm = new HungarianAlgorithm(costMatrixForHungarianAlgo); + + int[] assignments = hungarianAlgorithm.execute(); + int editorialCostForMapping = editorialCostForMapping(partialMapping, completeSourceGraph, completeTargetGraph, new ArrayList<>()); + double costMatrixSum = getCostMatrixSum(costMatrix, assignments); + + + double lowerBoundForPartialMapping = editorialCostForMapping + costMatrixSum; + int v_i_target_IndexSibling = assignments[0]; + Vertex bestExtensionTargetVertexSibling = availableTargetVertices.get(v_i_target_IndexSibling); + Mapping newMappingSibling = partialMapping.extendMapping(v_i, bestExtensionTargetVertexSibling); + + + if (lowerBoundForPartialMapping >= optimalEdit.ged) { + return; + } + MappingEntry newMappingEntry = new MappingEntry(newMappingSibling, level + 1, lowerBoundForPartialMapping); + LinkedBlockingQueue siblings = new LinkedBlockingQueue<>(); + newMappingEntry.mappingEntriesSiblings = siblings; + newMappingEntry.assignments = assignments; + newMappingEntry.availableTargetVertices = availableTargetVertices; + + queue.add(newMappingEntry); + Mapping fullMapping = partialMapping.copy(); + for (int i = 0; i < assignments.length; i++) { + fullMapping.add(sourceList.get(level + i), availableTargetVertices.get(assignments[i])); + } + + List editOperations = new ArrayList<>(); + int costForFullMapping = editorialCostForMapping(fullMapping, completeSourceGraph, completeTargetGraph, editOperations); + updateOptimalEdit(optimalEdit, costForFullMapping, fullMapping, editOperations); + + calculateRestOfChildren( + availableTargetVertices, + hungarianAlgorithm, + costMatrix, + editorialCostForMapping, + partialMapping, + v_i, + optimalEdit.ged, + level + 1, + siblings + ); + } + + private void updateOptimalEdit(OptimalEdit optimalEdit, int newGed, Mapping mapping, List editOperations) { + if (newGed < optimalEdit.ged) { + optimalEdit.ged = newGed; + + optimalEdit.listOfEditOperations.clear(); + optimalEdit.listOfEditOperations.add(editOperations); + + optimalEdit.listOfSets.clear(); + optimalEdit.listOfSets.add(new LinkedHashSet<>(editOperations)); + + optimalEdit.mappings.clear(); + optimalEdit.mappings.add(mapping); + System.out.println("setting new best edit at level " + (mapping.size()) + " with size " + editOperations.size()); + } else if (newGed == optimalEdit.ged) { + Set newSet = new LinkedHashSet<>(editOperations); + for (Set set : optimalEdit.listOfSets) { + if (set.equals(newSet)) { + return; + } + } + optimalEdit.listOfSets.add(newSet); + optimalEdit.listOfEditOperations.add(editOperations); + optimalEdit.mappings.add(mapping); + } + } + + // generate all children mappings and save in MappingEntry.sibling + private void calculateRestOfChildren(List availableTargetVertices, + HungarianAlgorithm hungarianAlgorithm, + AtomicDoubleArray[] costMatrixCopy, + double editorialCostForMapping, + Mapping partialMapping, + Vertex v_i, + int upperBound, + int level, + LinkedBlockingQueue siblings + ) { + // starting from 1 as we already generated the first one + for (int child = 1; child < availableTargetVertices.size(); child++) { + int[] assignments = hungarianAlgorithm.nextChild(); + if (hungarianAlgorithm.costMatrix[0].get(assignments[0]) == Integer.MAX_VALUE) { + break; + } + + double costMatrixSumSibling = getCostMatrixSum(costMatrixCopy, assignments); + double lowerBoundForPartialMappingSibling = editorialCostForMapping + costMatrixSumSibling; + int v_i_target_IndexSibling = assignments[0]; + Vertex bestExtensionTargetVertexSibling = availableTargetVertices.get(v_i_target_IndexSibling); + Mapping newMappingSibling = partialMapping.extendMapping(v_i, bestExtensionTargetVertexSibling); + + + if (lowerBoundForPartialMappingSibling >= upperBound) { + break; + } + MappingEntry sibling = new MappingEntry(newMappingSibling, level, lowerBoundForPartialMappingSibling); + sibling.mappingEntriesSiblings = siblings; + sibling.assignments = assignments; + sibling.availableTargetVertices = availableTargetVertices; + + siblings.add(sibling); + } + siblings.add(LAST_ELEMENT); + + } + + // this retrieves the next sibling from MappingEntry.sibling and adds it to the queue if the lowerBound is less than the current upperBound + private void addSiblingToQueue( + int level, + PriorityQueue queue, + OptimalEdit optimalEdit, + List sourceList, + List targetGraph, + MappingEntry mappingEntry) throws InterruptedException { + + MappingEntry sibling = mappingEntry.mappingEntriesSiblings.take(); + if (sibling == LAST_ELEMENT) { + mappingEntry.siblingsFinished = true; + return; + } + if (sibling.lowerBoundCost < optimalEdit.ged) { +// System.out.println("adding new sibling entry " + getDebugMap(sibling.partialMapping) + " at level " + level + " with candidates left: " + sibling.availableTargetVertices.size() + " at lower bound: " + sibling.lowerBoundCost); + + queue.add(sibling); + + // we need to start here from the parent mapping, this is why we remove the last element + Mapping fullMapping = sibling.partialMapping.removeLastElement(); + for (int i = 0; i < sibling.assignments.length; i++) { + fullMapping.add(sourceList.get(level - 1 + i), sibling.availableTargetVertices.get(sibling.assignments[i])); + } +// assertTrue(fullMapping.size() == this.sourceGraph.size()); + List editOperations = new ArrayList<>(); + int costForFullMapping = editorialCostForMapping(fullMapping, completeSourceGraph, completeTargetGraph, editOperations); + updateOptimalEdit(optimalEdit, costForFullMapping, fullMapping, editOperations); + } else { +// System.out.println("sibling not good enough"); + } + } + + + private double getCostMatrixSum(AtomicDoubleArray[] costMatrix, int[] assignments) { + double costMatrixSum = 0; + for (int i = 0; i < assignments.length; i++) { + costMatrixSum += costMatrix[i].get(assignments[i]); + } + return costMatrixSum; + } + + /** + * a partial mapping introduces a sub graph. The editorial cost is only calculated with respect to this sub graph. + */ + + // lower bound mapping cost between for v -> u in respect to a partial mapping + // this is BMa + private double calcLowerBoundMappingCost(Vertex v, + Vertex u, + List partialMappingSourceList, + Set partialMappingSourceSet, + List partialMappingTargetList, + Set partialMappingTargetSet + + ) { + if (!isolatedVertices.mappingPossible(v, u)) { + return Integer.MAX_VALUE; + } + boolean equalNodes = v.getType().equals(u.getType()) && v.getProperties().equals(u.getProperties()); + + // inner edge labels of u (resp. v) in regards to the partial mapping: all labels of edges + // which are adjacent of u (resp. v) which are inner edges + List adjacentEdgesV = completeSourceGraph.getAdjacentEdges(v); + Multiset multisetLabelsV = HashMultiset.create(); + + for (Edge edge : adjacentEdgesV) { + // test if this an inner edge: meaning both edges vertices are part of the non mapped vertices + // or: at least one edge is part of the partial mapping + if (!partialMappingSourceSet.contains(edge.getFrom()) && !partialMappingSourceSet.contains(edge.getTo())) { + multisetLabelsV.add(edge.getLabel()); + } + } + + List adjacentEdgesU = completeTargetGraph.getAdjacentEdges(u); + Multiset multisetLabelsU = HashMultiset.create(); + for (Edge edge : adjacentEdgesU) { + // test if this is an inner edge (meaning it not part of the subgraph induced by the partial mapping) + if (!partialMappingTargetSet.contains(edge.getFrom()) && !partialMappingTargetSet.contains(edge.getTo())) { + multisetLabelsU.add(edge.getLabel()); + } + } + + /** + * looking at all edges from x,vPrime and y,mappedVPrime + */ + int anchoredVerticesCost = 0; + for (int i = 0; i < partialMappingSourceList.size(); i++) { + Vertex vPrime = partialMappingSourceList.get(i); + Vertex mappedVPrime = partialMappingTargetList.get(i); + + Edge sourceEdge = completeSourceGraph.getEdge(v, vPrime); + String labelSourceEdge = sourceEdge != null ? sourceEdge.getLabel() : null; + Edge targetEdge = completeTargetGraph.getEdge(u, mappedVPrime); + String labelTargetEdge = targetEdge != null ? targetEdge.getLabel() : null; + if (!Objects.equals(labelSourceEdge, labelTargetEdge)) { + anchoredVerticesCost++; + } + + Edge sourceEdgeInverse = completeSourceGraph.getEdge(vPrime, v); + String labelSourceEdgeInverse = sourceEdgeInverse != null ? sourceEdgeInverse.getLabel() : null; + Edge targetEdgeInverse = completeTargetGraph.getEdge(mappedVPrime, u); + String labelTargetEdgeInverse = targetEdgeInverse != null ? targetEdgeInverse.getLabel() : null; + if (!Objects.equals(labelSourceEdgeInverse, labelTargetEdgeInverse)) { + anchoredVerticesCost++; + } + + } + + Multiset intersection = Multisets.intersection(multisetLabelsV, multisetLabelsU); + int multiSetEditDistance = Math.max(multisetLabelsV.size(), multisetLabelsU.size()) - intersection.size(); + + double result = (equalNodes ? 0 : 1) + multiSetEditDistance + anchoredVerticesCost; + return result; + } + + private List getDebugMap(Mapping mapping) { + List result = new ArrayList<>(); +// if (mapping.size() > 0) { +// result.add(mapping.getSource(mapping.size() - 1).getType() + " -> " + mapping.getTarget(mapping.size() - 1).getType()); +// } + for (Map.Entry entry : mapping.getMap().entrySet()) { +// if (!entry.getKey().getType().equals(entry.getValue().getType())) { +// result.add(entry.getKey().getType() + "->" + entry.getValue().getType()); +// } + result.add(entry.getKey().getDebugName() + "->" + entry.getValue().getDebugName()); + } + return result; + } + + +} diff --git a/src/main/java/graphql/schema/diffing/Edge.java b/src/main/java/graphql/schema/diffing/Edge.java new file mode 100644 index 0000000000..15a603ef62 --- /dev/null +++ b/src/main/java/graphql/schema/diffing/Edge.java @@ -0,0 +1,62 @@ +package graphql.schema.diffing; + +import graphql.Internal; + +import java.util.Objects; + +@Internal +public class Edge { + private Vertex from; + private Vertex to; + + private String label = ""; + + public Edge(Vertex from, Vertex to) { + this.from = from; + this.to = to; + } + + public Edge(Vertex from, Vertex to, String label) { + this.from = from; + this.to = to; + this.label = label; + } + + public Vertex getFrom() { + return from; + } + + public void setFrom(Vertex from) { + this.from = from; + } + + public Vertex getTo() { + return to; + } + + public void setTo(Vertex to) { + this.to = to; + } + + + public void setLabel(String label) { + this.label = label; + } + + public String getLabel() { + return label; + } + + @Override + public String toString() { + return "Edge{" + + "from=" + from + + ", to=" + to + + ", label='" + label + '\'' + + '}'; + } + + public boolean isEqualTo(Edge other) { + return Objects.equals(this.label, other.label); + } +} diff --git a/src/main/java/graphql/schema/diffing/EditOperation.java b/src/main/java/graphql/schema/diffing/EditOperation.java new file mode 100644 index 0000000000..7ee0ef8adc --- /dev/null +++ b/src/main/java/graphql/schema/diffing/EditOperation.java @@ -0,0 +1,107 @@ +package graphql.schema.diffing; + +import graphql.Internal; + +import java.util.Objects; + +@Internal +public class EditOperation { + + private EditOperation(Operation operation, + String description, + Vertex sourceVertex, + Vertex targetVertex, + Edge sourceEdge, + Edge targetEdge) { + this.operation = operation; + this.description = description; + this.sourceVertex = sourceVertex; + this.targetVertex = targetVertex; + this.sourceEdge = sourceEdge; + this.targetEdge = targetEdge; + } + + public static EditOperation deleteVertex(String description, Vertex sourceVertex,Vertex targetVertex) { + return new EditOperation(Operation.DELETE_VERTEX, description, sourceVertex, targetVertex, null, null); + } + + public static EditOperation insertVertex(String description,Vertex sourceVertex, Vertex targetVertex) { + return new EditOperation(Operation.INSERT_VERTEX, description, sourceVertex, targetVertex, null, null); + } + + public static EditOperation changeVertex(String description, Vertex sourceVertex, Vertex targetVertex) { + return new EditOperation(Operation.CHANGE_VERTEX, description, sourceVertex, targetVertex, null, null); + } + + public static EditOperation deleteEdge(String description, Edge sourceEdge) { + return new EditOperation(Operation.DELETE_EDGE, description, null, null, sourceEdge, null); + } + + public static EditOperation insertEdge(String description, Edge targetEdge) { + return new EditOperation(Operation.INSERT_EDGE, description, null, null, null, targetEdge); + } + + public static EditOperation changeEdge(String description, Edge sourceEdge, Edge targetEdge) { + return new EditOperation(Operation.CHANGE_EDGE, description, null, null, sourceEdge, targetEdge); + } + + private Operation operation; + private String description; + private Vertex sourceVertex; + private Vertex targetVertex; + private Edge sourceEdge; + private Edge targetEdge; + + + public enum Operation { + CHANGE_VERTEX, DELETE_VERTEX, INSERT_VERTEX, CHANGE_EDGE, INSERT_EDGE, DELETE_EDGE + } + + public Operation getOperation() { + return operation; + } + + + public Vertex getSourceVertex() { + return sourceVertex; + } + + public Vertex getTargetVertex() { + return targetVertex; + } + + public Edge getSourceEdge() { + return sourceEdge; + } + + public Edge getTargetEdge() { + return targetEdge; + } + + @Override + public String toString() { + return "EditOperation{" + + "operation=" + operation + + ", description='" + description + '\'' + + '}'; + } + + + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + EditOperation that = (EditOperation) o; + return operation == that.operation && Objects.equals(description, that.description) && Objects.equals(sourceVertex, that.sourceVertex) && Objects.equals(targetVertex, that.targetVertex) && Objects.equals(sourceEdge, that.sourceEdge) && Objects.equals(targetEdge, that.targetEdge); + } + + @Override + public int hashCode() { + return Objects.hash(operation, description, sourceVertex, targetVertex, sourceEdge, targetEdge); + } +} diff --git a/src/main/java/graphql/schema/diffing/EditorialCostForMapping.java b/src/main/java/graphql/schema/diffing/EditorialCostForMapping.java new file mode 100644 index 0000000000..4137f90cbc --- /dev/null +++ b/src/main/java/graphql/schema/diffing/EditorialCostForMapping.java @@ -0,0 +1,70 @@ +package graphql.schema.diffing; + +import graphql.Internal; + +import java.util.List; + +@Internal +public class EditorialCostForMapping { + + /** + * a mapping introduces a subgraph consisting of all vertices and all edges between these vertices + */ + public static int editorialCostForMapping(Mapping mapping, // can be a partial mapping + SchemaGraph sourceGraph, // the whole graph + SchemaGraph targetGraph, // the whole graph + List editOperationsResult) { + int cost = 0; + for (int i = 0; i < mapping.size(); i++) { + Vertex sourceVertex = mapping.getSource(i); + Vertex targetVertex = mapping.getTarget(i); + // Vertex changing (relabeling) + boolean equalNodes = sourceVertex.getType().equals(targetVertex.getType()) && sourceVertex.getProperties().equals(targetVertex.getProperties()); + if (!equalNodes) { + if (sourceVertex.isIsolated()) { + editOperationsResult.add(EditOperation.insertVertex("Insert" + targetVertex, sourceVertex, targetVertex)); + } else if (targetVertex.isIsolated()) { + editOperationsResult.add(EditOperation.deleteVertex("Delete " + sourceVertex, sourceVertex, targetVertex)); + } else { + editOperationsResult.add(EditOperation.changeVertex("Change " + sourceVertex + " to " + targetVertex, sourceVertex, targetVertex)); + } + cost++; + } + } + List edges = sourceGraph.getEdges(); + // edge deletion or relabeling + for (Edge sourceEdge : edges) { + // only edges relevant to the subgraph + if (!mapping.containsSource(sourceEdge.getFrom()) || !mapping.containsSource(sourceEdge.getTo())) { + continue; + } + Vertex targetFrom = mapping.getTarget(sourceEdge.getFrom()); + Vertex targetTo = mapping.getTarget(sourceEdge.getTo()); + Edge targetEdge = targetGraph.getEdge(targetFrom, targetTo); + if (targetEdge == null) { + editOperationsResult.add(EditOperation.deleteEdge("Delete edge " + sourceEdge, sourceEdge)); + cost++; + } else if (!sourceEdge.getLabel().equals(targetEdge.getLabel())) { + editOperationsResult.add(EditOperation.changeEdge("Change " + sourceEdge + " to " + targetEdge, sourceEdge, targetEdge)); + cost++; + } + } + + //TODO: iterates over all edges in the target Graph + for (Edge targetEdge : targetGraph.getEdges()) { + // only subgraph edges + if (!mapping.containsTarget(targetEdge.getFrom()) || !mapping.containsTarget(targetEdge.getTo())) { + continue; + } + Vertex sourceFrom = mapping.getSource(targetEdge.getFrom()); + Vertex sourceTo = mapping.getSource(targetEdge.getTo()); + if (sourceGraph.getEdge(sourceFrom, sourceTo) == null) { + editOperationsResult.add(EditOperation.insertEdge("Insert edge " + targetEdge, targetEdge)); + cost++; + } + } + return cost; + } + + +} diff --git a/src/main/java/graphql/schema/diffing/FillupIsolatedVertices.java b/src/main/java/graphql/schema/diffing/FillupIsolatedVertices.java new file mode 100644 index 0000000000..0f27e76ce7 --- /dev/null +++ b/src/main/java/graphql/schema/diffing/FillupIsolatedVertices.java @@ -0,0 +1,926 @@ +package graphql.schema.diffing; + +import com.google.common.collect.BiMap; +import com.google.common.collect.HashBasedTable; +import com.google.common.collect.HashBiMap; +import com.google.common.collect.HashMultimap; +import com.google.common.collect.ImmutableList; +import com.google.common.collect.Multimap; +import com.google.common.collect.Table; +import graphql.Assert; +import graphql.Internal; +import graphql.util.FpKit; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collection; +import java.util.Collections; +import java.util.LinkedHashMap; +import java.util.LinkedHashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; + +import static graphql.schema.diffing.SchemaGraph.APPLIED_ARGUMENT; +import static graphql.schema.diffing.SchemaGraph.APPLIED_DIRECTIVE; +import static graphql.schema.diffing.SchemaGraph.ARGUMENT; +import static graphql.schema.diffing.SchemaGraph.DIRECTIVE; +import static graphql.schema.diffing.SchemaGraph.ENUM; +import static graphql.schema.diffing.SchemaGraph.ENUM_VALUE; +import static graphql.schema.diffing.SchemaGraph.FIELD; +import static graphql.schema.diffing.SchemaGraph.INPUT_FIELD; +import static graphql.schema.diffing.SchemaGraph.INPUT_OBJECT; +import static graphql.schema.diffing.SchemaGraph.INTERFACE; +import static graphql.schema.diffing.SchemaGraph.OBJECT; +import static graphql.schema.diffing.SchemaGraph.SCALAR; +import static graphql.schema.diffing.SchemaGraph.SCHEMA; +import static graphql.schema.diffing.SchemaGraph.UNION; +import static graphql.util.FpKit.concat; +import static java.util.Collections.emptyList; +import static java.util.Collections.singletonList; + +@Internal +public class FillupIsolatedVertices { + + SchemaGraph sourceGraph; + SchemaGraph targetGraph; + IsolatedVertices isolatedVertices; + + private BiMap toRemove = HashBiMap.create(); + + static Map> typeContexts = new LinkedHashMap<>(); + + static { + typeContexts.put(SCHEMA, schemaContext()); + typeContexts.put(FIELD, fieldContext()); + typeContexts.put(ARGUMENT, argumentsContexts()); + typeContexts.put(INPUT_FIELD, inputFieldContexts()); + typeContexts.put(OBJECT, objectContext()); + typeContexts.put(INTERFACE, interfaceContext()); + typeContexts.put(UNION, unionContext()); + typeContexts.put(INPUT_OBJECT, inputObjectContext()); + typeContexts.put(SCALAR, scalarContext()); + typeContexts.put(ENUM, enumContext()); + typeContexts.put(ENUM_VALUE, enumValueContext()); + typeContexts.put(APPLIED_DIRECTIVE, appliedDirectiveContext()); + typeContexts.put(APPLIED_ARGUMENT, appliedArgumentContext()); + typeContexts.put(DIRECTIVE, directiveContext()); + } + + private static List inputFieldContexts() { + VertexContextSegment inputFieldType = new VertexContextSegment() { + @Override + public String idForVertex(Vertex vertex, SchemaGraph schemaGraph) { + return vertex.getType(); + } + + @Override + public boolean filter(Vertex vertex, SchemaGraph schemaGraph) { + return INPUT_FIELD.equals(vertex.getType()); + } + }; + VertexContextSegment inputObjectContext = new VertexContextSegment() { + @Override + public String idForVertex(Vertex inputField, SchemaGraph schemaGraph) { + Vertex inputObject = schemaGraph.getInputObjectForInputField(inputField); + return inputObject.getName(); + } + + @Override + public boolean filter(Vertex vertex, SchemaGraph schemaGraph) { + return true; + } + }; + VertexContextSegment inputFieldName = new VertexContextSegment() { + @Override + public String idForVertex(Vertex inputField, SchemaGraph schemaGraph) { + return inputField.getName(); + } + + @Override + public boolean filter(Vertex vertex, SchemaGraph schemaGraph) { + return true; + } + }; + List contexts = Arrays.asList(inputFieldType, inputObjectContext, inputFieldName); + return contexts; + } + + + private static List scalarContext() { + VertexContextSegment scalar = new VertexContextSegment() { + @Override + public String idForVertex(Vertex vertex, SchemaGraph schemaGraph) { + return vertex.getType(); + } + + @Override + public boolean filter(Vertex vertex, SchemaGraph schemaGraph) { + return SCALAR.equals(vertex.getType()); + } + }; + VertexContextSegment scalarName = new VertexContextSegment() { + @Override + public String idForVertex(Vertex vertex, SchemaGraph schemaGraph) { + return vertex.getName(); + } + + @Override + public boolean filter(Vertex vertex, SchemaGraph schemaGraph) { + return true; + } + }; + List contexts = Arrays.asList(scalar, scalarName); + return contexts; + } + + private static List inputObjectContext() { + VertexContextSegment inputObject = new VertexContextSegment() { + @Override + public String idForVertex(Vertex vertex, SchemaGraph schemaGraph) { + return vertex.getType(); + } + + @Override + public boolean filter(Vertex vertex, SchemaGraph schemaGraph) { + return INPUT_OBJECT.equals(vertex.getType()); + } + }; + VertexContextSegment inputObjectName = new VertexContextSegment() { + @Override + public String idForVertex(Vertex inputObject, SchemaGraph schemaGraph) { + return inputObject.getName(); + } + + @Override + public boolean filter(Vertex vertex, SchemaGraph schemaGraph) { + return true; + } + }; + List contexts = Arrays.asList(inputObject, inputObjectName); + return contexts; + } + + private static List objectContext() { + VertexContextSegment objectType = new VertexContextSegment() { + @Override + public String idForVertex(Vertex vertex, SchemaGraph schemaGraph) { + return vertex.getType(); + } + + @Override + public boolean filter(Vertex vertex, SchemaGraph schemaGraph) { + return OBJECT.equals(vertex.getType()); + } + }; + + VertexContextSegment objectName = new VertexContextSegment() { + @Override + public String idForVertex(Vertex object, SchemaGraph schemaGraph) { + return object.getName(); + } + + @Override + public boolean filter(Vertex vertex, SchemaGraph schemaGraph) { + return true; + } + }; + List contexts = Arrays.asList(objectType, objectName); + return contexts; + } + + private static List enumContext() { + VertexContextSegment enumCtxType = new VertexContextSegment() { + @Override + public String idForVertex(Vertex vertex, SchemaGraph schemaGraph) { + return vertex.getType(); + } + + @Override + public boolean filter(Vertex vertex, SchemaGraph schemaGraph) { + return ENUM.equals(vertex.getType()); + } + }; + VertexContextSegment enumName = new VertexContextSegment() { + @Override + public String idForVertex(Vertex enumVertex, SchemaGraph schemaGraph) { + return enumVertex.getName(); + } + + @Override + public boolean filter(Vertex vertex, SchemaGraph schemaGraph) { + return true; + } + }; + List contexts = Arrays.asList(enumCtxType, enumName); + return contexts; + } + + private static List enumValueContext() { + VertexContextSegment enumValueType = new VertexContextSegment() { + @Override + public String idForVertex(Vertex vertex, SchemaGraph schemaGraph) { + return vertex.getType(); + } + + @Override + public boolean filter(Vertex vertex, SchemaGraph schemaGraph) { + return ENUM_VALUE.equals(vertex.getType()); + } + }; + VertexContextSegment enumName = new VertexContextSegment() { + @Override + public String idForVertex(Vertex enumValue, SchemaGraph schemaGraph) { + return schemaGraph.getEnumForEnumValue(enumValue).getName(); + } + + @Override + public boolean filter(Vertex vertex, SchemaGraph schemaGraph) { + return true; + } + }; + VertexContextSegment enumValueName = new VertexContextSegment() { + @Override + public String idForVertex(Vertex enumValue, SchemaGraph schemaGraph) { + return enumValue.getName(); + } + + @Override + public boolean filter(Vertex vertex, SchemaGraph schemaGraph) { + return true; + } + }; + List contexts = Arrays.asList(enumValueType, enumName, enumValueName); + return contexts; + } + + private static List interfaceContext() { + VertexContextSegment interfaceType = new VertexContextSegment() { + @Override + public String idForVertex(Vertex vertex, SchemaGraph schemaGraph) { + return vertex.getType(); + } + + @Override + public boolean filter(Vertex vertex, SchemaGraph schemaGraph) { + return INTERFACE.equals(vertex.getType()); + } + }; + VertexContextSegment interfaceName = new VertexContextSegment() { + @Override + public String idForVertex(Vertex interfaceVertex, SchemaGraph schemaGraph) { + return interfaceVertex.getName(); + } + + @Override + public boolean filter(Vertex vertex, SchemaGraph schemaGraph) { + return true; + } + }; + + List contexts = Arrays.asList(interfaceType, interfaceName); + return contexts; + } + + private static List unionContext() { + VertexContextSegment unionType = new VertexContextSegment() { + @Override + public String idForVertex(Vertex vertex, SchemaGraph schemaGraph) { + return vertex.getType(); + } + + @Override + public boolean filter(Vertex vertex, SchemaGraph schemaGraph) { + return UNION.equals(vertex.getType()); + } + }; + VertexContextSegment unionName = new VertexContextSegment() { + @Override + public String idForVertex(Vertex union, SchemaGraph schemaGraph) { + return union.getName(); + } + + @Override + public boolean filter(Vertex vertex, SchemaGraph schemaGraph) { + return true; + } + }; + + List contexts = Arrays.asList(unionType, unionName); + return contexts; + } + + private static List directiveContext() { + VertexContextSegment directiveType = new VertexContextSegment() { + @Override + public String idForVertex(Vertex vertex, SchemaGraph schemaGraph) { + return vertex.getType(); + } + + @Override + public boolean filter(Vertex vertex, SchemaGraph schemaGraph) { + return DIRECTIVE.equals(vertex.getType()); + } + }; + VertexContextSegment directiveName = new VertexContextSegment() { + @Override + public String idForVertex(Vertex directive, SchemaGraph schemaGraph) { + return directive.getName(); + } + + @Override + public boolean filter(Vertex vertex, SchemaGraph schemaGraph) { + return true; + } + }; + + List contexts = Arrays.asList(directiveType, directiveName); + return contexts; + } + + private static List appliedDirectiveContext() { + VertexContextSegment appliedDirectiveType = new VertexContextSegment() { + @Override + public String idForVertex(Vertex vertex, SchemaGraph schemaGraph) { + return vertex.getType(); + } + + @Override + public boolean filter(Vertex vertex, SchemaGraph schemaGraph) { + return APPLIED_DIRECTIVE.equals(vertex.getType()); + } + }; + VertexContextSegment appliedDirectiveName = new VertexContextSegment() { + @Override + public String idForVertex(Vertex appliedDirective, SchemaGraph schemaGraph) { + int appliedDirectiveIndex = schemaGraph.getAppliedDirectiveIndex(appliedDirective); + return appliedDirectiveIndex + ":" + appliedDirective.getName(); + } + + @Override + public boolean filter(Vertex vertex, SchemaGraph schemaGraph) { + return true; + } + }; + + VertexContextSegment appliedDirectiveContainer = new VertexContextSegment() { + @Override + public String idForVertex(Vertex appliedDirective, SchemaGraph schemaGraph) { + Vertex appliedDirectiveContainer = schemaGraph.getAppliedDirectiveContainerForAppliedDirective(appliedDirective); + return appliedDirectiveContainer.getType() + "." + appliedDirectiveContainer.getName(); + } + + @Override + public boolean filter(Vertex vertex, SchemaGraph schemaGraph) { + return true; + } + }; + VertexContextSegment parentOfContainer = new VertexContextSegment() { + @Override + public String idForVertex(Vertex appliedDirective, SchemaGraph schemaGraph) { + Vertex container = schemaGraph.getAppliedDirectiveContainerForAppliedDirective(appliedDirective); + switch (container.getType()) { + case SCHEMA: + return SCHEMA; + case FIELD: + Vertex fieldsContainer = schemaGraph.getFieldsContainerForField(container); + return fieldsContainer.getType() + "." + fieldsContainer.getName(); + case OBJECT: + return OBJECT; + case INTERFACE: + return INTERFACE; + case INPUT_FIELD: + Vertex inputObject = schemaGraph.getInputObjectForInputField(container); + return inputObject.getType() + "." + inputObject.getName(); + case ARGUMENT: + Vertex fieldOrDirective = schemaGraph.getFieldOrDirectiveForArgument(container); + return fieldOrDirective.getType() + "." + fieldOrDirective.getName(); + case INPUT_OBJECT: + return INPUT_OBJECT; + case ENUM: + return ENUM; + case UNION: + return UNION; + case SCALAR: + return SCALAR; + case ENUM_VALUE: + Vertex enumVertex = schemaGraph.getEnumForEnumValue(container); + return enumVertex.getType() + "." + enumVertex.getName(); + default: + throw new IllegalStateException("Unexpected directive container type " + container.getType()); + } + } + + @Override + public boolean filter(Vertex vertex, SchemaGraph schemaGraph) { + return true; + } + }; + + VertexContextSegment parentOfParentOfContainer = new VertexContextSegment() { + @Override + public String idForVertex(Vertex appliedDirective, SchemaGraph schemaGraph) { + Vertex container = schemaGraph.getAppliedDirectiveContainerForAppliedDirective(appliedDirective); + switch (container.getType()) { + case SCHEMA: + case FIELD: + case OBJECT: + case INTERFACE: + case INPUT_FIELD: + case INPUT_OBJECT: + case ENUM: + case ENUM_VALUE: + case UNION: + case SCALAR: + return ""; + case ARGUMENT: + Vertex fieldOrDirective = schemaGraph.getFieldOrDirectiveForArgument(container); + switch (fieldOrDirective.getType()) { + case FIELD: + Vertex fieldsContainer = schemaGraph.getFieldsContainerForField(fieldOrDirective); + return fieldsContainer.getType() + "." + fieldsContainer.getName(); + case DIRECTIVE: + return ""; + default: + return Assert.assertShouldNeverHappen(); + } + default: + throw new IllegalStateException("Unexpected directive container type " + container.getType()); + } + } + + @Override + public boolean filter(Vertex vertex, SchemaGraph schemaGraph) { + return true; + } + }; + List contexts = Arrays.asList(appliedDirectiveType, parentOfParentOfContainer, parentOfContainer, appliedDirectiveContainer, appliedDirectiveName); + return contexts; + } + + + private static List appliedArgumentContext() { + VertexContextSegment appliedArgumentType = new VertexContextSegment() { + @Override + public String idForVertex(Vertex vertex, SchemaGraph schemaGraph) { + return vertex.getType(); + } + + @Override + public boolean filter(Vertex vertex, SchemaGraph schemaGraph) { + return APPLIED_ARGUMENT.equals(vertex.getType()); + } + }; + VertexContextSegment appliedDirective = new VertexContextSegment() { + @Override + public String idForVertex(Vertex appliedArgument, SchemaGraph schemaGraph) { + Vertex appliedDirective = schemaGraph.getAppliedDirectiveForAppliedArgument(appliedArgument); + int appliedDirectiveIndex = schemaGraph.getAppliedDirectiveIndex(appliedDirective); + return appliedDirectiveIndex + ":" + appliedDirective.getName(); + } + + @Override + public boolean filter(Vertex vertex, SchemaGraph schemaGraph) { + return true; + } + }; + + VertexContextSegment appliedDirectiveContainer = new VertexContextSegment() { + @Override + public String idForVertex(Vertex appliedArgument, SchemaGraph schemaGraph) { + Vertex appliedDirective = schemaGraph.getAppliedDirectiveForAppliedArgument(appliedArgument); + Vertex appliedDirectiveContainer = schemaGraph.getAppliedDirectiveContainerForAppliedDirective(appliedDirective); + return appliedDirectiveContainer.getName(); + } + + @Override + public boolean filter(Vertex vertex, SchemaGraph schemaGraph) { + return true; + } + }; + VertexContextSegment parentOfContainer = new VertexContextSegment() { + @Override + public String idForVertex(Vertex appliedArgument, SchemaGraph schemaGraph) { + Vertex appliedDirective = schemaGraph.getAppliedDirectiveForAppliedArgument(appliedArgument); + Vertex container = schemaGraph.getAppliedDirectiveContainerForAppliedDirective(appliedDirective); + switch (container.getType()) { + case SCHEMA: + return SCHEMA; + case FIELD: + Vertex fieldsContainer = schemaGraph.getFieldsContainerForField(container); + return fieldsContainer.getType() + "." + fieldsContainer.getName(); + case OBJECT: + return OBJECT; + case INTERFACE: + return INTERFACE; + case INPUT_FIELD: + Vertex inputObject = schemaGraph.getInputObjectForInputField(container); + return inputObject.getType() + "." + inputObject.getName(); + case ARGUMENT: + Vertex fieldOrDirective = schemaGraph.getFieldOrDirectiveForArgument(container); + return fieldOrDirective.getType() + "." + fieldOrDirective.getName(); + case INPUT_OBJECT: + return INPUT_OBJECT; + case ENUM: + return ENUM; + case UNION: + return UNION; + case SCALAR: + return SCALAR; + case ENUM_VALUE: + Vertex enumVertex = schemaGraph.getEnumForEnumValue(container); + return enumVertex.getType() + "." + enumVertex.getName(); + default: + throw new IllegalStateException("Unexpected directive container type " + container.getType()); + } + } + + @Override + public boolean filter(Vertex vertex, SchemaGraph schemaGraph) { + return true; + } + }; + + VertexContextSegment parentOfParentOfContainer = new VertexContextSegment() { + @Override + public String idForVertex(Vertex appliedArgument, SchemaGraph schemaGraph) { + Vertex appliedDirective = schemaGraph.getAppliedDirectiveForAppliedArgument(appliedArgument); + Vertex container = schemaGraph.getAppliedDirectiveContainerForAppliedDirective(appliedDirective); + switch (container.getType()) { + case SCHEMA: + case FIELD: + case OBJECT: + case INTERFACE: + case INPUT_FIELD: + case INPUT_OBJECT: + case ENUM: + case ENUM_VALUE: + case UNION: + case SCALAR: + return ""; + case ARGUMENT: + Vertex fieldOrDirective = schemaGraph.getFieldOrDirectiveForArgument(container); + switch (fieldOrDirective.getType()) { + case FIELD: + Vertex fieldsContainer = schemaGraph.getFieldsContainerForField(fieldOrDirective); + return fieldsContainer.getType() + "." + fieldsContainer.getName(); + case DIRECTIVE: + return ""; + default: + return Assert.assertShouldNeverHappen(); + } + default: + throw new IllegalStateException("Unexpected directive container type " + container.getType()); + } + } + + @Override + public boolean filter(Vertex vertex, SchemaGraph schemaGraph) { + return true; + } + }; + VertexContextSegment appliedArgumentName = new VertexContextSegment() { + @Override + public String idForVertex(Vertex appliedArgument, SchemaGraph schemaGraph) { + return appliedArgument.getName(); + } + + @Override + public boolean filter(Vertex vertex, SchemaGraph schemaGraph) { + return true; + } + }; + List contexts = Arrays.asList(appliedArgumentType, parentOfParentOfContainer, parentOfContainer, appliedDirectiveContainer, appliedDirective, appliedArgumentName); + return contexts; + } + + private static List schemaContext() { + VertexContextSegment schema = new VertexContextSegment() { + @Override + public String idForVertex(Vertex vertex, SchemaGraph schemaGraph) { + return vertex.getType(); + } + + @Override + public boolean filter(Vertex vertex, SchemaGraph schemaGraph) { + return SCHEMA.equals(vertex.getType()); + } + }; + return singletonList(schema); + } + + private static List fieldContext() { + VertexContextSegment field = new VertexContextSegment() { + @Override + public String idForVertex(Vertex vertex, SchemaGraph schemaGraph) { + return vertex.getType(); + } + + @Override + public boolean filter(Vertex vertex, SchemaGraph schemaGraph) { + return FIELD.equals(vertex.getType()); + } + }; + VertexContextSegment container = new VertexContextSegment() { + @Override + public String idForVertex(Vertex field, SchemaGraph schemaGraph) { + Vertex fieldsContainer = schemaGraph.getFieldsContainerForField(field); + return fieldsContainer.getType() + "." + fieldsContainer.getName(); + } + + @Override + public boolean filter(Vertex vertex, SchemaGraph schemaGraph) { + return true; + } + }; + + VertexContextSegment fieldName = new VertexContextSegment() { + @Override + public String idForVertex(Vertex field, SchemaGraph schemaGraph) { + return field.getName(); + } + + @Override + public boolean filter(Vertex vertex, SchemaGraph schemaGraph) { + return true; + } + }; + List contexts = Arrays.asList(field, container, fieldName); + return contexts; + } + + private static List argumentsContexts() { + + VertexContextSegment argumentType = new VertexContextSegment() { + @Override + public String idForVertex(Vertex vertex, SchemaGraph schemaGraph) { + return vertex.getType(); + } + + @Override + public boolean filter(Vertex vertex, SchemaGraph schemaGraph) { + return ARGUMENT.equals(vertex.getType()); + } + }; + + VertexContextSegment fieldOrDirective = new VertexContextSegment() { + @Override + public String idForVertex(Vertex argument, SchemaGraph schemaGraph) { + Vertex fieldOrDirective = schemaGraph.getFieldOrDirectiveForArgument(argument); + return fieldOrDirective.getType() + "." + fieldOrDirective.getName(); + } + + @Override + public boolean filter(Vertex argument, SchemaGraph schemaGraph) { + return true; + } + }; + VertexContextSegment containerOrDirectiveHolder = new VertexContextSegment() { + @Override + public String idForVertex(Vertex argument, SchemaGraph schemaGraph) { + Vertex fieldOrDirective = schemaGraph.getFieldOrDirectiveForArgument(argument); + if (fieldOrDirective.getType().equals(FIELD)) { + Vertex fieldsContainer = schemaGraph.getFieldsContainerForField(fieldOrDirective); + // can be Interface or Object + return fieldsContainer.getType() + "." + fieldsContainer.getName(); + } else { + // a directive doesn't have further context + return ""; + } + } + + @Override + public boolean filter(Vertex argument, SchemaGraph schemaGraph) { + return true; + } + }; + VertexContextSegment argumentName = new VertexContextSegment() { + @Override + public String idForVertex(Vertex argument, SchemaGraph schemaGraph) { + return argument.getName(); + } + + @Override + public boolean filter(Vertex argument, SchemaGraph schemaGraph) { + return true; + } + }; + List contexts = Arrays.asList(argumentType, containerOrDirectiveHolder, fieldOrDirective, argumentName); + return contexts; + } + + + public FillupIsolatedVertices(SchemaGraph sourceGraph, SchemaGraph targetGraph) { + this.sourceGraph = sourceGraph; + this.targetGraph = targetGraph; + this.isolatedVertices = new IsolatedVertices(); + } + + public void ensureGraphAreSameSize() { + calcPossibleMappings(typeContexts.get(SCHEMA), SCHEMA); + calcPossibleMappings(typeContexts.get(FIELD), FIELD); + calcPossibleMappings(typeContexts.get(ARGUMENT), ARGUMENT); + calcPossibleMappings(typeContexts.get(INPUT_FIELD), INPUT_FIELD); +// calcPossibleMappings(typeContexts.get(DUMMY_TYPE_VERTEX), DUMMY_TYPE_VERTEX); + calcPossibleMappings(typeContexts.get(OBJECT), OBJECT); + calcPossibleMappings(typeContexts.get(INTERFACE), INTERFACE); + calcPossibleMappings(typeContexts.get(UNION), UNION); + calcPossibleMappings(typeContexts.get(INPUT_OBJECT), INPUT_OBJECT); + calcPossibleMappings(typeContexts.get(SCALAR), SCALAR); + calcPossibleMappings(typeContexts.get(ENUM), ENUM); + calcPossibleMappings(typeContexts.get(ENUM_VALUE), ENUM_VALUE); + calcPossibleMappings(typeContexts.get(APPLIED_DIRECTIVE), APPLIED_DIRECTIVE); + calcPossibleMappings(typeContexts.get(APPLIED_ARGUMENT), APPLIED_ARGUMENT); + calcPossibleMappings(typeContexts.get(DIRECTIVE), DIRECTIVE); + + + sourceGraph.addVertices(isolatedVertices.allIsolatedSource); + targetGraph.addVertices(isolatedVertices.allIsolatedTarget); + + Assert.assertTrue(sourceGraph.size() == targetGraph.size()); + } + + + public abstract static class VertexContextSegment { + + private List children; + + public VertexContextSegment(List children) { + this.children = children; + } + + public VertexContextSegment() { + this.children = emptyList(); + } + + public VertexContextSegment(VertexContextSegment child) { + this.children = singletonList(child); + } + + public abstract String idForVertex(Vertex vertex, SchemaGraph schemaGraph); + + public abstract boolean filter(Vertex vertex, SchemaGraph schemaGraph); + } + + public class IsolatedVertices { + + public Set allIsolatedSource = new LinkedHashSet<>(); + public Set allIsolatedTarget = new LinkedHashSet<>(); + + public Table, Set, Set> contexts = HashBasedTable.create(); + + public Multimap possibleMappings = HashMultimap.create(); + public Mapping mapping = new Mapping(); + + public void putPossibleMappings(Collection sourceVertices, Collection targetVertex) { + for (Vertex sourceVertex : sourceVertices) { + possibleMappings.putAll(sourceVertex, targetVertex); + } + } + + public void addIsolatedSource(Collection isolatedSource) { + allIsolatedSource.addAll(isolatedSource); + } + + public void addIsolatedTarget(Collection isolatedTarget) { + allIsolatedTarget.addAll(isolatedTarget); + } + + // + public boolean mappingPossible(Vertex sourceVertex, Vertex targetVertex) { + return possibleMappings.containsEntry(sourceVertex, targetVertex); + } + + public void putContext(List contextId, Set source, Set target) { + if (contexts.containsRow(contextId)) { + throw new IllegalArgumentException("Already context " + contextId); + } + Assert.assertTrue(source.size() == target.size()); + if (source.size() == 1) { + mapping.add(source.iterator().next(), target.iterator().next()); + } + contexts.put(contextId, source, target); + } + + } + + + public void calcPossibleMappings(List contexts, String typeNameForDebug) { + Collection currentSourceVertices = sourceGraph.getVertices(); + Collection currentTargetVertices = targetGraph.getVertices(); + calcPossibleMappingImpl(currentSourceVertices, + currentTargetVertices, + Collections.emptyList(), + 0, + contexts, + new LinkedHashSet<>(), + new LinkedHashSet<>(), + typeNameForDebug); + } + + /** + * calc for the provided context + */ + private void calcPossibleMappingImpl( + Collection currentSourceVertices, + Collection currentTargetVertices, + List contextId, + int contextIx, + List contexts, + Set usedSourceVertices, + Set usedTargetVertices, + String typeNameForDebug) { + + VertexContextSegment finalCurrentContext = contexts.get(contextIx); + Map> sourceGroups = FpKit.filterAndGroupingBy(currentSourceVertices, + v -> finalCurrentContext.filter(v, sourceGraph), + v -> finalCurrentContext.idForVertex(v, sourceGraph)); + Map> targetGroups = FpKit.filterAndGroupingBy(currentTargetVertices, + v -> finalCurrentContext.filter(v, targetGraph), + v -> finalCurrentContext.idForVertex(v, targetGraph)); + + + List deletedContexts = new ArrayList<>(); + List insertedContexts = new ArrayList<>(); + List sameContexts = new ArrayList<>(); + Util.diffNamedList(sourceGroups.keySet(), targetGroups.keySet(), deletedContexts, insertedContexts, sameContexts); + + // for each unchanged context we descend recursively into + for (String sameContext : sameContexts) { + ImmutableList sourceVerticesInContext = sourceGroups.get(sameContext); + ImmutableList targetVerticesInContext = targetGroups.get(sameContext); + + List currentContextId = concat(contextId, sameContext); + if (contexts.size() > contextIx + 1) { + calcPossibleMappingImpl(sourceVerticesInContext, targetVerticesInContext, currentContextId, contextIx + 1, contexts, usedSourceVertices, usedTargetVertices, typeNameForDebug); + } + /** + * Either there was no context segment left or not all vertices were relevant for + * Either way: fill up with isolated vertices and record as possible mapping + */ + Set notUsedSource = new LinkedHashSet<>(sourceVerticesInContext); + notUsedSource.removeAll(usedSourceVertices); + Set notUsedTarget = new LinkedHashSet<>(targetVerticesInContext); + notUsedTarget.removeAll(usedTargetVertices); + + // make sure the current context is the same size + if (notUsedSource.size() > notUsedTarget.size()) { + Set newTargetVertices = Vertex.newIsolatedNodes(notUsedSource.size() - notUsedTarget.size(), "target-isolated-" + typeNameForDebug + "-"); + isolatedVertices.addIsolatedTarget(newTargetVertices); + notUsedTarget.addAll(newTargetVertices); + } else if (notUsedTarget.size() > notUsedSource.size()) { + Set newSourceVertices = Vertex.newIsolatedNodes(notUsedTarget.size() - notUsedSource.size(), "source-isolated-" + typeNameForDebug + "-"); + isolatedVertices.addIsolatedSource(newSourceVertices); + notUsedSource.addAll(newSourceVertices); + } + isolatedVertices.putPossibleMappings(notUsedSource, notUsedTarget); + usedSourceVertices.addAll(notUsedSource); + usedTargetVertices.addAll(notUsedTarget); + if (notUsedSource.size() > 0) { + isolatedVertices.putContext(currentContextId, notUsedSource, notUsedTarget); + } + } + + Set possibleTargetVertices = new LinkedHashSet<>(); + for (String insertedContext : insertedContexts) { + ImmutableList vertices = targetGroups.get(insertedContext); + for (Vertex targetVertex : vertices) { + if (!usedTargetVertices.contains(targetVertex)) { + possibleTargetVertices.add(targetVertex); + } + } + usedTargetVertices.addAll(vertices); + } + + Set possibleSourceVertices = new LinkedHashSet<>(); + for (String deletedContext : deletedContexts) { + ImmutableList vertices = sourceGroups.get(deletedContext); + for (Vertex sourceVertex : vertices) { + if (!usedSourceVertices.contains(sourceVertex)) { + possibleSourceVertices.add(sourceVertex); + } + } + usedSourceVertices.addAll(vertices); + } + + if (possibleSourceVertices.size() > possibleTargetVertices.size()) { + Set newTargetVertices = Vertex.newIsolatedNodes(possibleSourceVertices.size() - possibleTargetVertices.size(), "target-isolated-" + typeNameForDebug + "-"); + isolatedVertices.addIsolatedTarget(newTargetVertices); + possibleTargetVertices.addAll(newTargetVertices); + } else if (possibleTargetVertices.size() > possibleSourceVertices.size()) { + Set newSourceVertices = Vertex.newIsolatedNodes(possibleTargetVertices.size() - possibleSourceVertices.size(), "source-isolated-" + typeNameForDebug + "-"); + isolatedVertices.addIsolatedSource(newSourceVertices); + possibleSourceVertices.addAll(newSourceVertices); + } + // if there are only added or removed vertices in the current context, contextId might be empty + if (possibleSourceVertices.size() > 0) { + if (contextId.size() == 0) { + contextId = singletonList(typeNameForDebug); + } + isolatedVertices.putContext(contextId, possibleSourceVertices, possibleTargetVertices); + } + isolatedVertices.putPossibleMappings(possibleSourceVertices, possibleTargetVertices); + } + + +} diff --git a/src/main/java/graphql/schema/diffing/GraphPrinter.java b/src/main/java/graphql/schema/diffing/GraphPrinter.java new file mode 100644 index 0000000000..da713fa22a --- /dev/null +++ b/src/main/java/graphql/schema/diffing/GraphPrinter.java @@ -0,0 +1,23 @@ +package graphql.schema.diffing; + +import graphql.Internal; +import graphql.schema.diffing.dot.Dotfile; + +@Internal +public class GraphPrinter { + + public static String print(SchemaGraph schemaGraph) { + Dotfile dotfile = new Dotfile(); + for (Vertex vertex : schemaGraph.getVertices()) { + String name = vertex.get("name"); + if (name == null) { + name = vertex.getType(); + } + dotfile.addNode("V" + Integer.toHexString(vertex.hashCode()), name, "blue"); + } + for (Edge edge : schemaGraph.getEdges()) { + dotfile.addEdge("V" + Integer.toHexString(edge.getFrom().hashCode()), "V" + Integer.toHexString(edge.getTo().hashCode()), edge.getLabel()); + } + return dotfile.print(); + } +} diff --git a/src/main/java/graphql/schema/diffing/HungarianAlgorithm.java b/src/main/java/graphql/schema/diffing/HungarianAlgorithm.java new file mode 100644 index 0000000000..6476352792 --- /dev/null +++ b/src/main/java/graphql/schema/diffing/HungarianAlgorithm.java @@ -0,0 +1,377 @@ +package graphql.schema.diffing; + +import com.google.common.util.concurrent.AtomicDoubleArray; +import graphql.Internal; + +import java.util.Arrays; + +/* Copyright (c) 2012 Kevin L. Stern + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ + +/** + * An implementation of the Hungarian algorithm for solving the assignment + * problem. An instance of the assignment problem consists of a number of + * workers along with a number of jobs and a cost matrix which gives the cost of + * assigning the i'th worker to the j'th job at position (i, j). The goal is to + * find an assignment of workers to jobs so that no job is assigned more than + * one worker and so that no worker is assigned to more than one job in such a + * manner so as to minimize the total cost of completing the jobs. + *

+ *

+ * An assignment for a cost matrix that has more workers than jobs will + * necessarily include unassigned workers, indicated by an assignment value of + * -1; in no other circumstance will there be unassigned workers. Similarly, an + * assignment for a cost matrix that has more jobs than workers will necessarily + * include unassigned jobs; in no other circumstance will there be unassigned + * jobs. For completeness, an assignment for a square cost matrix will give + * exactly one unique worker to each job. + *

+ *

+ * This version of the Hungarian algorithm runs in time O(n^3), where n is the + * maximum among the number of workers and the number of jobs. + * + * @author Kevin L. Stern + */ +@Internal +public class HungarianAlgorithm { + // changed by reduce + public final AtomicDoubleArray[] costMatrix; + + // constant always + private final int rows; + private final int cols; + private final int dim; + + // the assigned workers,jobs for the result + public final int[] matchJobByWorker; + private final int[] matchWorkerByJob; + + // reset for each execute + private final int[] minSlackWorkerByJob; + private final double[] minSlackValueByJob; + private final int[] parentWorkerByCommittedJob; + // reset for worker + private final boolean[] committedWorkers; + + + // labels for both sides of the bipartite graph + private final double[] labelByWorker; + private final double[] labelByJob; + + + /** + * Construct an instance of the algorithm. + * + * @param costMatrix the cost matrix, where matrix[i][j] holds the cost of assigning + * worker i to job j, for all i, j. The cost matrix must not be + * irregular in the sense that all rows must be the same length; in + * addition, all entries must be non-infinite numbers. + */ + public HungarianAlgorithm(AtomicDoubleArray[] costMatrix) { + this.dim = Math.max(costMatrix.length, costMatrix[0].length()); + this.rows = costMatrix.length; + this.cols = costMatrix[0].length(); + this.costMatrix = costMatrix; +// for (int w = 0; w < this.dim; w++) { +// if (w < costMatrix.length) { +// if (costMatrix[w].length() != this.cols) { +// throw new IllegalArgumentException("Irregular cost matrix"); +// } +//// for (int j = 0; j < this.cols; j++) { +//// if (Double.isInfinite(costMatrix[w].get(j))) { +//// throw new IllegalArgumentException("Infinite cost"); +//// } +//// if (Double.isNaN(costMatrix[w].get(j))) { +//// throw new IllegalArgumentException("NaN cost"); +//// } +//// } +// this.costMatrix[w] = costMatrix(costMatrix[w], this.dim); +// } else { +// this.costMatrix[w] = new double[this.dim]; +// } +// } + labelByWorker = new double[this.dim]; + labelByJob = new double[this.dim]; + minSlackWorkerByJob = new int[this.dim]; + minSlackValueByJob = new double[this.dim]; + committedWorkers = new boolean[this.dim]; + parentWorkerByCommittedJob = new int[this.dim]; + matchJobByWorker = new int[this.dim]; + Arrays.fill(matchJobByWorker, -1); + matchWorkerByJob = new int[this.dim]; + Arrays.fill(matchWorkerByJob, -1); + } + + /** + * Compute an initial feasible solution by assigning zero labels to the + * workers and by assigning to each job a label equal to the minimum cost + * among its incident edges. + */ + protected void computeInitialFeasibleSolution() { + for (int j = 0; j < dim; j++) { + labelByJob[j] = Double.POSITIVE_INFINITY; + } + for (int w = 0; w < dim; w++) { + for (int j = 0; j < dim; j++) { + if (costMatrix[w].get(j) < labelByJob[j]) { + labelByJob[j] = costMatrix[w].get(j); + } + } + } + } + + /** + * Execute the algorithm. + * + * @return the minimum cost matching of workers to jobs based upon the + * provided cost matrix. A matching value of -1 indicates that the + * corresponding worker is unassigned. + */ + public int[] execute() { + /* + * Heuristics to improve performance: Reduce rows and columns by their + * smallest element, compute an initial non-zero dual feasible solution and + * create a greedy matching from workers to jobs of the cost matrix. + */ + reduce(); + computeInitialFeasibleSolution(); + greedyMatch(); + + int w = fetchUnmatchedWorker(); + while (w < dim) { + initializePhase(w); + executePhase(); + w = fetchUnmatchedWorker(); + } + int[] result = Arrays.copyOf(matchJobByWorker, rows); + for (w = 0; w < result.length; w++) { + if (result[w] >= cols) { + result[w] = -1; + } + } + return result; + } + + /** + * Execute a single phase of the algorithm. A phase of the Hungarian algorithm + * consists of building a set of committed workers and a set of committed jobs + * from a root unmatched worker by following alternating unmatched/matched + * zero-slack edges. If an unmatched job is encountered, then an augmenting + * path has been found and the matching is grown. If the connected zero-slack + * edges have been exhausted, the labels of committed workers are increased by + * the minimum slack among committed workers and non-committed jobs to create + * more zero-slack edges (the labels of committed jobs are simultaneously + * decreased by the same amount in order to maintain a feasible labeling). + *

+ *

+ * The runtime of a single phase of the algorithm is O(n^2), where n is the + * dimension of the internal square cost matrix, since each edge is visited at + * most once and since increasing the labeling is accomplished in time O(n) by + * maintaining the minimum slack values among non-committed jobs. When a phase + * completes, the matching will have increased in size. + */ + protected void executePhase() { + while (true) { + // the last worker we found + int minSlackWorker = -1; + int minSlackJob = -1; + double minSlackValue = Double.POSITIVE_INFINITY; + for (int j = 0; j < dim; j++) { + if (parentWorkerByCommittedJob[j] == -1) { + if (minSlackValueByJob[j] < minSlackValue) { + minSlackValue = minSlackValueByJob[j]; + minSlackWorker = minSlackWorkerByJob[j]; + minSlackJob = j; + } + } + } + if (minSlackValue > 0) { + updateLabeling(minSlackValue); + } + // adding (minSlackWorker, minSlackJob) to the path + parentWorkerByCommittedJob[minSlackJob] = minSlackWorker; + // check if minSlackJob is not assigned yet + // the requirement of an augmenting path is that start and end is not matched (assigned) yet + if (matchWorkerByJob[minSlackJob] == -1) { + /* + * An augmenting path has been found. + * Iterating via parentWorkerByCommittedJob and matchJobByWorker through the + * path and add/update matching. + * Job -> Parent worker via parentWorkerByCommittedJob + * and Worker -> Job via matchJobByWorker + */ + int committedJob = minSlackJob; + int parentWorker = parentWorkerByCommittedJob[committedJob]; + while (true) { + int temp = matchJobByWorker[parentWorker]; + match(parentWorker, committedJob); + committedJob = temp; + if (committedJob == -1) { + break; + } + parentWorker = parentWorkerByCommittedJob[committedJob]; + } + return; + } else { + /* + * Update slack values since we increased the size of the committed + * workers set. + */ + // we checked above that minSlackJob is indeed assigned + int worker = matchWorkerByJob[minSlackJob]; + // committedWorkers is used when slack is updated + committedWorkers[worker] = true; + for (int j = 0; j < dim; j++) { + if (parentWorkerByCommittedJob[j] == -1) { + double slack = costMatrix[worker].get(j) - labelByWorker[worker] + - labelByJob[j]; + if (minSlackValueByJob[j] > slack) { + minSlackValueByJob[j] = slack; + minSlackWorkerByJob[j] = worker; + } + } + } + } + } + } + + /** + * @return the first unmatched worker or {@link #dim} if none. + */ + protected int fetchUnmatchedWorker() { + int w; + for (w = 0; w < dim; w++) { + if (matchJobByWorker[w] == -1) { + break; + } + } + return w; + } + + /** + * Find a valid matching by greedily selecting among zero-cost matchings. This + * is a heuristic to jump-start the augmentation algorithm. + */ + protected void greedyMatch() { + for (int w = 0; w < dim; w++) { + for (int j = 0; j < dim; j++) { + if (matchJobByWorker[w] == -1 && matchWorkerByJob[j] == -1 + && costMatrix[w].get(j) - labelByWorker[w] - labelByJob[j] == 0) { + match(w, j); + } + } + } + } + + /** + * Initialize the next phase of the algorithm by clearing the committed + * workers and jobs sets and by initializing the slack arrays to the values + * corresponding to the specified root worker. + * + * @param w the worker at which to root the next phase. + */ + protected void initializePhase(int w) { + Arrays.fill(committedWorkers, false); + Arrays.fill(parentWorkerByCommittedJob, -1); + committedWorkers[w] = true; + for (int j = 0; j < dim; j++) { + minSlackValueByJob[j] = costMatrix[w].get(j) - labelByWorker[w] - labelByJob[j]; + minSlackWorkerByJob[j] = w; + } + } + + /** + * Helper method to record a matching between worker w and job j. + */ + protected void match(int w, int j) { + matchJobByWorker[w] = j; + matchWorkerByJob[j] = w; + } + + /** + * Reduce the cost matrix by subtracting the smallest element of each row from + * all elements of the row as well as the smallest element of each column from + * all elements of the column. Note that an optimal assignment for a reduced + * cost matrix is optimal for the original cost matrix. + */ + protected void reduce() { + for (int w = 0; w < dim; w++) { + double min = Double.POSITIVE_INFINITY; + for (int j = 0; j < dim; j++) { + if (costMatrix[w].get(j) < min) { + min = costMatrix[w].get(j); + } + } + for (int j = 0; j < dim; j++) { + costMatrix[w].set(j, costMatrix[w].get(j) - min); + } + } + double[] min = new double[dim]; + for (int j = 0; j < dim; j++) { + min[j] = Double.POSITIVE_INFINITY; + } + for (int w = 0; w < dim; w++) { + for (int j = 0; j < dim; j++) { + if (costMatrix[w].get(j) < min[j]) { + min[j] = costMatrix[w].get(j); + } + } + } + for (int w = 0; w < dim; w++) { + for (int j = 0; j < dim; j++) { + costMatrix[w].set(j, costMatrix[w].get(j) - min[j]); + } + } + } + + /** + * Update labels with the specified slack by adding the slack value for + * committed workers and by subtracting the slack value for committed jobs. In + * addition, update the minimum slack values appropriately. + */ + protected void updateLabeling(double slack) { + for (int w = 0; w < dim; w++) { + if (committedWorkers[w]) { + labelByWorker[w] += slack; + } + } + for (int j = 0; j < dim; j++) { + if (parentWorkerByCommittedJob[j] != -1) { + labelByJob[j] -= slack; + } else { + minSlackValueByJob[j] -= slack; + } + } + } + + public int[] nextChild() { + int currentJobAssigned = matchJobByWorker[0]; + // we want to make currentJobAssigned not allowed,meaning we set the size to Infinity + costMatrix[0].set(currentJobAssigned, Integer.MAX_VALUE); + matchWorkerByJob[currentJobAssigned] = -1; + matchJobByWorker[0] = -1; + minSlackValueByJob[currentJobAssigned] = Integer.MAX_VALUE; + initializePhase(0); + executePhase(); + int[] result = Arrays.copyOf(matchJobByWorker, rows); + return result; + } +} \ No newline at end of file diff --git a/src/main/java/graphql/schema/diffing/Mapping.java b/src/main/java/graphql/schema/diffing/Mapping.java new file mode 100644 index 0000000000..42ec164f03 --- /dev/null +++ b/src/main/java/graphql/schema/diffing/Mapping.java @@ -0,0 +1,114 @@ +package graphql.schema.diffing; + +import com.google.common.collect.BiMap; +import com.google.common.collect.HashBiMap; +import graphql.Internal; + +import java.util.ArrayList; +import java.util.List; +import java.util.Objects; + +@Internal +public class Mapping { + private BiMap map = HashBiMap.create(); + private List sourceList = new ArrayList<>(); + private List targetList = new ArrayList<>(); + + private Mapping(BiMap map, List sourceList, List targetList) { + this.map = map; + this.sourceList = sourceList; + this.targetList = targetList; + } + + public Mapping() { + + } + + public Vertex getSource(Vertex target) { + return map.inverse().get(target); + } + + public Vertex getTarget(Vertex source) { + return map.get(source); + } + + public Vertex getSource(int i) { + return sourceList.get(i); + } + + public Vertex getTarget(int i) { + return targetList.get(i); + } + + public List getTargets() { + return targetList; + } + + public List getSources() { + return sourceList; + } + + public boolean containsSource(Vertex sourceVertex) { + return map.containsKey(sourceVertex); + } + + public boolean containsTarget(Vertex targetVertex) { + return map.containsValue(targetVertex); + } + + public int size() { + return map.size(); + } + + public void add(Vertex source, Vertex target) { + this.map.put(source, target); + this.sourceList.add(source); + this.targetList.add(target); + } + + public Mapping removeLastElement() { + HashBiMap newMap = HashBiMap.create(map); + newMap.remove(this.sourceList.get(this.sourceList.size() - 1)); + List newSourceList = new ArrayList<>(this.sourceList.subList(0, this.sourceList.size() - 1)); + List newTargetList = new ArrayList<>(this.targetList.subList(0, this.targetList.size() - 1)); + return new Mapping(newMap, newSourceList, newTargetList); + } + + public Mapping copy() { + HashBiMap newMap = HashBiMap.create(map); + List newSourceList = new ArrayList<>(this.sourceList); + List newTargetList = new ArrayList<>(this.targetList); + return new Mapping(newMap, newSourceList, newTargetList); + } + + public Mapping extendMapping(Vertex source, Vertex target) { + HashBiMap newMap = HashBiMap.create(map); + newMap.put(source, target); + List newSourceList = new ArrayList<>(this.sourceList); + newSourceList.add(source); + List newTargetList = new ArrayList<>(this.targetList); + newTargetList.add(target); + return new Mapping(newMap, newSourceList, newTargetList); + } + + public BiMap getMap() { + return map; + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + Mapping mapping = (Mapping) o; + return Objects.equals(map, mapping.map); + } + + @Override + public int hashCode() { + return Objects.hash(map); + } +} diff --git a/src/main/java/graphql/schema/diffing/SchemaDiffing.java b/src/main/java/graphql/schema/diffing/SchemaDiffing.java new file mode 100644 index 0000000000..763ed0b1b3 --- /dev/null +++ b/src/main/java/graphql/schema/diffing/SchemaDiffing.java @@ -0,0 +1,146 @@ +package graphql.schema.diffing; + +import graphql.Internal; +import graphql.schema.GraphQLSchema; +import graphql.schema.diffing.ana.EditOperationAnalysisResult; +import graphql.schema.diffing.ana.EditOperationAnalyzer; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; + +import static graphql.Assert.assertTrue; +import static graphql.schema.diffing.EditorialCostForMapping.editorialCostForMapping; +import static java.util.Collections.singletonList; + +@Internal +public class SchemaDiffing { + + + + SchemaGraph sourceGraph; + SchemaGraph targetGraph; + + public List diffGraphQLSchema(GraphQLSchema graphQLSchema1, GraphQLSchema graphQLSchema2) throws Exception { + sourceGraph = new SchemaGraphFactory("source-").createGraph(graphQLSchema1); + targetGraph = new SchemaGraphFactory("target-").createGraph(graphQLSchema2); + return diffImpl(sourceGraph, targetGraph).listOfEditOperations.get(0); + } + + public EditOperationAnalysisResult diffAndAnalyze(GraphQLSchema graphQLSchema1, GraphQLSchema graphQLSchema2) throws Exception { + sourceGraph = new SchemaGraphFactory("source-").createGraph(graphQLSchema1); + targetGraph = new SchemaGraphFactory("target-").createGraph(graphQLSchema2); + DiffImpl.OptimalEdit optimalEdit = diffImpl(sourceGraph, targetGraph); + EditOperationAnalyzer editOperationAnalyzer = new EditOperationAnalyzer(graphQLSchema1, graphQLSchema1, sourceGraph, targetGraph); + return editOperationAnalyzer.analyzeEdits(optimalEdit.listOfEditOperations.get(0),optimalEdit.mappings.get(0)); + } + + public DiffImpl.OptimalEdit diffGraphQLSchemaAllEdits(GraphQLSchema graphQLSchema1, GraphQLSchema graphQLSchema2) throws Exception { + sourceGraph = new SchemaGraphFactory("source-").createGraph(graphQLSchema1); + targetGraph = new SchemaGraphFactory("target-").createGraph(graphQLSchema2); + return diffImpl(sourceGraph, targetGraph); + } + + + private DiffImpl.OptimalEdit diffImpl(SchemaGraph sourceGraph, SchemaGraph targetGraph) throws Exception { + int sizeDiff = targetGraph.size() - sourceGraph.size(); + System.out.println("graph diff: " + sizeDiff); + FillupIsolatedVertices fillupIsolatedVertices = new FillupIsolatedVertices(sourceGraph, targetGraph); + fillupIsolatedVertices.ensureGraphAreSameSize(); + FillupIsolatedVertices.IsolatedVertices isolatedVertices = fillupIsolatedVertices.isolatedVertices; + + assertTrue(sourceGraph.size() == targetGraph.size()); +// if (sizeDiff != 0) { +// SortSourceGraph.sortSourceGraph(sourceGraph, targetGraph, isolatedVertices); +// } + Mapping fixedMappings = isolatedVertices.mapping; + System.out.println("fixed mappings: " + fixedMappings.size() + " vs " + sourceGraph.size()); + if (fixedMappings.size() == sourceGraph.size()) { + List result = new ArrayList<>(); + editorialCostForMapping(fixedMappings, sourceGraph, targetGraph, result); + return new DiffImpl.OptimalEdit(singletonList(fixedMappings), singletonList(result), result.size()); + } + DiffImpl diffImpl = new DiffImpl(sourceGraph, targetGraph, isolatedVertices); + List nonMappedSource = new ArrayList<>(sourceGraph.getVertices()); + nonMappedSource.removeAll(fixedMappings.getSources()); +// for(Vertex vertex: nonMappedSource) { +// System.out.println("non mapped: " + vertex); +// } +// for (List context : isolatedVertices.contexts.rowKeySet()) { +// Map, Set> row = isolatedVertices.contexts.row(context); +// System.out.println("context: " + context + " from " + row.keySet().iterator().next().size() + " to " + row.values().iterator().next().size()); +// } + + List nonMappedTarget = new ArrayList<>(targetGraph.getVertices()); + nonMappedTarget.removeAll(fixedMappings.getTargets()); + + sortListBasedOnPossibleMapping(nonMappedSource, isolatedVertices); + + // the non mapped vertices go to the end + List sourceVertices = new ArrayList<>(); + sourceVertices.addAll(fixedMappings.getSources()); + sourceVertices.addAll(nonMappedSource); + + List targetGraphVertices = new ArrayList<>(); + targetGraphVertices.addAll(fixedMappings.getTargets()); + targetGraphVertices.addAll(nonMappedTarget); + + + DiffImpl.OptimalEdit optimalEdit = diffImpl.diffImpl(fixedMappings, sourceVertices, targetGraphVertices); +// System.out.println("different edit counts: " + optimalEdit.listOfEditOperations.size()); +// for (int i = 0; i < optimalEdit.listOfEditOperations.size(); i++) { +// System.out.println("--------------"); +// System.out.println("edit: " + i); +// System.out.println("--------------"); +// for (EditOperation editOperation : optimalEdit.listOfEditOperations.get(i)) { +// System.out.println(editOperation); +// } +// System.out.println("--------------"); +// System.out.println("--------------"); +// } + return optimalEdit; + } + + private void sortListBasedOnPossibleMapping(List sourceVertices, FillupIsolatedVertices.IsolatedVertices isolatedVertices) { + Collections.sort(sourceVertices, (v1, v2) -> + { + int v2Count = isolatedVertices.possibleMappings.get(v2).size(); + int v1Count = isolatedVertices.possibleMappings.get(v1).size(); + return Integer.compare(v2Count, v1Count); + }); + +// for (Vertex vertex : sourceGraph.getVertices()) { +// System.out.println("c: " + isolatedVertices.possibleMappings.get(vertex).size() + " v: " + vertex); +// } + } + + + private List calcEdgeOperations(Mapping mapping) { + List edges = sourceGraph.getEdges(); + List result = new ArrayList<>(); + // edge deletion or relabeling + for (Edge sourceEdge : edges) { + Vertex target1 = mapping.getTarget(sourceEdge.getFrom()); + Vertex target2 = mapping.getTarget(sourceEdge.getTo()); + Edge targetEdge = targetGraph.getEdge(target1, target2); + if (targetEdge == null) { + result.add(EditOperation.deleteEdge("Delete edge " + sourceEdge, sourceEdge)); + } else if (!sourceEdge.getLabel().equals(targetEdge.getLabel())) { + result.add(EditOperation.changeEdge("Change " + sourceEdge + " to " + targetEdge, sourceEdge, targetEdge)); + } + } + + //TODO: iterates over all edges in the target Graph + for (Edge targetEdge : targetGraph.getEdges()) { + // only subgraph edges + Vertex sourceFrom = mapping.getSource(targetEdge.getFrom()); + Vertex sourceTo = mapping.getSource(targetEdge.getTo()); + if (sourceGraph.getEdge(sourceFrom, sourceTo) == null) { + result.add(EditOperation.insertEdge("Insert edge " + targetEdge, targetEdge)); + } + } + return result; + } + + +} diff --git a/src/main/java/graphql/schema/diffing/SchemaGraph.java b/src/main/java/graphql/schema/diffing/SchemaGraph.java new file mode 100644 index 0000000000..eb64c3e4ef --- /dev/null +++ b/src/main/java/graphql/schema/diffing/SchemaGraph.java @@ -0,0 +1,274 @@ +package graphql.schema.diffing; + + +import com.google.common.collect.HashBasedTable; +import com.google.common.collect.LinkedHashMultimap; +import com.google.common.collect.Multimap; +import com.google.common.collect.Table; +import graphql.ExperimentalApi; +import org.jetbrains.annotations.Nullable; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import java.util.Optional; +import java.util.function.Predicate; + +import static graphql.Assert.assertTrue; +import static java.lang.String.format; + +@ExperimentalApi +public class SchemaGraph { + + public static final String SCHEMA = "Schema"; + public static final String OBJECT = "Object"; + public static final String INTERFACE = "Interface"; + public static final String UNION = "Union"; + public static final String FIELD = "Field"; + public static final String ARGUMENT = "Argument"; + public static final String SCALAR = "Scalar"; + public static final String ENUM = "Enum"; + public static final String ENUM_VALUE = "EnumValue"; + public static final String INPUT_OBJECT = "InputObject"; + public static final String INPUT_FIELD = "InputField"; + public static final String DIRECTIVE = "Directive"; + public static final String APPLIED_DIRECTIVE = "AppliedDirective"; + public static final String APPLIED_ARGUMENT = "AppliedArgument"; + public static final String ISOLATED = "__ISOLATED"; + + + private List vertices = new ArrayList<>(); + private List edges = new ArrayList<>(); + + private Map typesByName = new LinkedHashMap<>(); + private Map directivesByName = new LinkedHashMap<>(); + private Table edgesByDirection = HashBasedTable.create(); + private Table edgesByInverseDirection = HashBasedTable.create(); + private Multimap typeToVertices = LinkedHashMultimap.create(); + + public SchemaGraph() { + + } + + public SchemaGraph(List vertices, List edges, Table edgeByVertexPair) { + this.vertices = vertices; + this.edges = edges; + this.edgesByDirection = edgeByVertexPair; + } + + public void addVertex(Vertex vertex) { + vertices.add(vertex); + typeToVertices.put(vertex.getType(), vertex); + } + + public void addVertices(Collection vertices) { + for (Vertex vertex : vertices) { + this.vertices.add(vertex); + typeToVertices.put(vertex.getType(), vertex); + } + } + + public Collection getVerticesByType(String type) { + return typeToVertices.get(type); + } + + public Multimap getVerticesByType() { + return typeToVertices; + } + + public void addEdge(Edge edge) { + edges.add(edge); + edgesByDirection.put(edge.getFrom(), edge.getTo(), edge); + edgesByInverseDirection.put(edge.getTo(), edge.getFrom(), edge); + } + + public List getAdjacentEdges(Vertex from) { + return new ArrayList<>(edgesByDirection.row(from).values()); + } + + public List getAdjacentEdgesAndInverse(Vertex fromAndTo) { + List result = new ArrayList<>(edgesByDirection.row(fromAndTo).values()); + result.addAll(edgesByInverseDirection.row(fromAndTo).values()); + return result; + } + + public List getAdjacentVertices(Vertex from) { + return getAdjacentVertices(from, x -> true); + } + + public List getAdjacentVertices(Vertex from, Predicate predicate) { + List result = new ArrayList<>(); + for (Edge edge : edgesByDirection.row(from).values()) { + Vertex v = edge.getTo(); + if (predicate.test(v)) { + result.add(v); + } + } + return result; + } + + public List getAdjacentVerticesInverse(Vertex to) { + return getAdjacentVerticesInverse(to, x -> true); + } + + public List getAdjacentVerticesInverse(Vertex to, Predicate predicate) { + List result = new ArrayList<>(); + for (Edge edge : edgesByInverseDirection.row(to).values()) { + Vertex v = edge.getFrom(); + if (predicate.test(v)) { + result.add(v); + } + } + return result; + } + + public List getAdjacentEdges(Vertex from, Predicate predicate) { + List result = new ArrayList<>(); + for (Edge edge : edgesByDirection.row(from).values()) { + Vertex v = edge.getTo(); + if (predicate.test(v)) { + result.add(edge); + } + } + return result; + } + + public List getAdjacentEdgesInverse(Vertex to) { + return getAdjacentEdgesInverse(to, x -> true); + } + + public List getAdjacentEdgesInverse(Vertex to, Predicate predicate) { + List result = new ArrayList<>(); + for (Edge edge : edgesByInverseDirection.row(to).values()) { + Vertex v = edge.getFrom(); + if (predicate.test(v)) { + result.add(edge); + } + } + return result; + } + + public Edge getSingleAdjacentEdge(Vertex from, Predicate predicate) { + for (Edge edge : edgesByDirection.row(from).values()) { + if (predicate.test(edge)) { + return edge; + } + } + return null; + } + + public List getEdges() { + return edges; + } + + // null if the edge doesn't exist + public @Nullable Edge getEdge(Vertex from, Vertex to) { + return edgesByDirection.get(from, to); + } + + public @Nullable Edge getEdgeOrInverse(Vertex from, Vertex to) { + Edge result = edgesByDirection.get(from, to); + return result != null ? result : edgesByInverseDirection.get(from, to); + } + + public List getVertices() { + return vertices; + } + + public void setVertices(List vertices) { + this.vertices = vertices; + } + + public void addType(String name, Vertex vertex) { + typesByName.put(name, vertex); + } + + public void addDirective(String name, Vertex vertex) { + directivesByName.put(name, vertex); + } + + public Vertex getType(String name) { + return typesByName.get(name); + } + + public Vertex getDirective(String name) { + return directivesByName.get(name); + } + + public Optional findTargetVertex(Vertex from, Predicate vertexPredicate) { + return edgesByDirection.row(from).values().stream().map(Edge::getTo).filter(vertexPredicate).findFirst(); + } + + public int size() { + return vertices.size(); + } + + public List addIsolatedVertices(int count, String debugPrefix) { + List result = new ArrayList<>(); + for (int i = 0; i < count; i++) { + Vertex isolatedVertex = Vertex.newIsolatedNode(debugPrefix + i); + vertices.add(isolatedVertex); + result.add(isolatedVertex); + } + return result; + } + + public Vertex getFieldOrDirectiveForArgument(Vertex argument) { + List adjacentVertices = getAdjacentVerticesInverse(argument); + assertTrue(adjacentVertices.size() == 1, () -> format("No field or directive found for %s", argument)); + return adjacentVertices.get(0); + } + + public Vertex getFieldsContainerForField(Vertex field) { + List adjacentVertices = getAdjacentVerticesInverse(field); + assertTrue(adjacentVertices.size() == 1, () -> format("No fields container found for %s", field)); + return adjacentVertices.get(0); + } + + public Vertex getInputObjectForInputField(Vertex inputField) { + List adjacentVertices = this.getAdjacentVerticesInverse(inputField); + assertTrue(adjacentVertices.size() == 1, () -> format("No input object found for %s", inputField)); + return adjacentVertices.get(0); + } + + public Vertex getAppliedDirectiveForAppliedArgument(Vertex appliedArgument) { + List adjacentVertices = this.getAdjacentVerticesInverse(appliedArgument); + assertTrue(adjacentVertices.size() == 1, () -> format("No applied directive found for %s", appliedArgument)); + return adjacentVertices.get(0); + } + + public Vertex getAppliedDirectiveContainerForAppliedDirective(Vertex appliedDirective) { + List adjacentVertices = this.getAdjacentVerticesInverse(appliedDirective); + assertTrue(adjacentVertices.size() == 1, () -> format("No applied directive container found for %s", appliedDirective)); + return adjacentVertices.get(0); + } + + public int getAppliedDirectiveIndex(Vertex appliedDirective) { + List adjacentEdges = this.getAdjacentEdgesInverse(appliedDirective); + assertTrue(adjacentEdges.size() == 1, () -> format("No applied directive container found for %s", appliedDirective)); + return Integer.parseInt(adjacentEdges.get(0).getLabel()); + } + + + public Vertex getEnumForEnumValue(Vertex enumValue) { + List adjacentVertices = this.getAdjacentVerticesInverse(enumValue); + assertTrue(adjacentVertices.size() == 1, () -> format("No enum found for %s", enumValue)); + return adjacentVertices.get(0); + } + + + public List getAllAdjacentEdges(List fromList, Vertex to) { + List result = new ArrayList<>(); + for (Vertex from : fromList) { + Edge edge = getEdge(from, to); + if (edge == null) { + continue; + } + result.add(edge); + } + return result; + } + +} diff --git a/src/main/java/graphql/schema/diffing/SchemaGraphFactory.java b/src/main/java/graphql/schema/diffing/SchemaGraphFactory.java new file mode 100644 index 0000000000..37ac8d5e31 --- /dev/null +++ b/src/main/java/graphql/schema/diffing/SchemaGraphFactory.java @@ -0,0 +1,420 @@ +package graphql.schema.diffing; + +import graphql.GraphQLContext; +import graphql.Internal; +import graphql.execution.ValuesResolver; +import graphql.introspection.Introspection; +import graphql.language.AstPrinter; +import graphql.schema.*; +import graphql.schema.idl.DirectiveInfo; +import graphql.schema.idl.ScalarInfo; +import graphql.util.TraversalControl; +import graphql.util.Traverser; +import graphql.util.TraverserContext; +import graphql.util.TraverserVisitor; + +import java.util.ArrayList; +import java.util.LinkedHashMap; +import java.util.LinkedHashSet; +import java.util.List; +import java.util.Locale; +import java.util.Map; +import java.util.Set; + +import static graphql.Assert.assertNotNull; + +@Internal +public class SchemaGraphFactory { + + private int counter = 1; + private final String debugPrefix; + + public SchemaGraphFactory(String debugPrefix) { + this.debugPrefix = debugPrefix; + } + + public SchemaGraphFactory() { + this.debugPrefix = ""; + } + + public SchemaGraph createGraph(GraphQLSchema schema) { + Set roots = new LinkedHashSet<>(); + roots.add(schema.getQueryType()); + if (schema.isSupportingMutations()) { + roots.add(schema.getMutationType()); + } + if (schema.isSupportingSubscriptions()) { + roots.add(schema.getSubscriptionType()); + } + roots.addAll(schema.getAdditionalTypes()); + roots.addAll(schema.getDirectives()); + roots.addAll(schema.getSchemaDirectives()); + roots.add(schema.getIntrospectionSchemaType()); + Traverser traverser = Traverser.depthFirst(GraphQLSchemaElement::getChildren); + SchemaGraph schemaGraph = new SchemaGraph(); + class IntrospectionNode { + + } + traverser.traverse(roots, new TraverserVisitor() { + @Override + public TraversalControl enter(TraverserContext context) { + boolean isIntrospectionNode = false; + if (context.thisNode() instanceof GraphQLNamedType) { + if (Introspection.isIntrospectionTypes((GraphQLNamedType) context.thisNode())) { + isIntrospectionNode = true; + context.setVar(IntrospectionNode.class, new IntrospectionNode()); + } + } else { + isIntrospectionNode = context.getVarFromParents(IntrospectionNode.class) != null; + } + if (context.thisNode() instanceof GraphQLObjectType) { + newObject((GraphQLObjectType) context.thisNode(), schemaGraph, isIntrospectionNode); + } + if (context.thisNode() instanceof GraphQLInterfaceType) { + newInterface((GraphQLInterfaceType) context.thisNode(), schemaGraph, isIntrospectionNode); + } + if (context.thisNode() instanceof GraphQLUnionType) { + newUnion((GraphQLUnionType) context.thisNode(), schemaGraph, isIntrospectionNode); + } + if (context.thisNode() instanceof GraphQLScalarType) { + newScalar((GraphQLScalarType) context.thisNode(), schemaGraph, isIntrospectionNode); + } + if (context.thisNode() instanceof GraphQLInputObjectType) { + newInputObject((GraphQLInputObjectType) context.thisNode(), schemaGraph, isIntrospectionNode); + } + if (context.thisNode() instanceof GraphQLEnumType) { + newEnum((GraphQLEnumType) context.thisNode(), schemaGraph, isIntrospectionNode); + } + if (context.thisNode() instanceof GraphQLDirective) { + // only continue if not applied directive + if (context.getParentNode() == null) { + newDirective((GraphQLDirective) context.thisNode(), schemaGraph); + } + } + return TraversalControl.CONTINUE; + } + + @Override + public TraversalControl leave(TraverserContext context) { + return TraversalControl.CONTINUE; + } + }); + addSchemaVertex(schemaGraph, schema); + + ArrayList copyOfVertices = new ArrayList<>(schemaGraph.getVertices()); + for (Vertex vertex : copyOfVertices) { + if (SchemaGraph.OBJECT.equals(vertex.getType())) { + handleObjectVertex(vertex, schemaGraph, schema); + } + if (SchemaGraph.INTERFACE.equals(vertex.getType())) { + handleInterfaceVertex(vertex, schemaGraph, schema); + } + if (SchemaGraph.UNION.equals(vertex.getType())) { + handleUnion(vertex, schemaGraph, schema); + } + if (SchemaGraph.INPUT_OBJECT.equals(vertex.getType())) { + handleInputObject(vertex, schemaGraph, schema); + } + if (SchemaGraph.DIRECTIVE.equals(vertex.getType())) { + handleDirective(vertex, schemaGraph, schema); + } + } + return schemaGraph; + } + + private void addSchemaVertex(SchemaGraph schemaGraph, GraphQLSchema graphQLSchema) { + GraphQLObjectType queryType = graphQLSchema.getQueryType(); + GraphQLObjectType mutationType = graphQLSchema.getMutationType(); + GraphQLObjectType subscriptionType = graphQLSchema.getSubscriptionType(); + Vertex schemaVertex = new Vertex(SchemaGraph.SCHEMA, "schema"); + schemaVertex.add("name", SchemaGraph.SCHEMA); + schemaGraph.addVertex(schemaVertex); + + Vertex queryVertex = schemaGraph.getType(queryType.getName()); + schemaGraph.addEdge(new Edge(schemaVertex, queryVertex, "query")); + if (mutationType != null) { + Vertex mutationVertex = schemaGraph.getType(mutationType.getName()); + schemaGraph.addEdge(new Edge(schemaVertex, mutationVertex, "mutation")); + } + if (subscriptionType != null) { + Vertex subscriptionVertex = schemaGraph.getType(subscriptionType.getName()); + schemaGraph.addEdge(new Edge(schemaVertex, subscriptionVertex, "subscription")); + } + createAppliedDirectives(schemaVertex, graphQLSchema.getSchemaDirectives(), schemaGraph); + } + + private void handleInputObject(Vertex inputObject, SchemaGraph schemaGraph, GraphQLSchema graphQLSchema) { + GraphQLInputObjectType inputObjectType = (GraphQLInputObjectType) graphQLSchema.getType(inputObject.get("name")); + List inputFields = inputObjectType.getFields(); + for (GraphQLInputObjectField inputField : inputFields) { + Vertex inputFieldVertex = schemaGraph.findTargetVertex(inputObject, vertex -> vertex.getType().equals("InputField") && + vertex.get("name").equals(inputField.getName())).get(); + handleInputField(inputFieldVertex, inputField, schemaGraph, graphQLSchema); + } + } + + private void handleInputField(Vertex inputFieldVertex, GraphQLInputObjectField inputField, SchemaGraph + schemaGraph, GraphQLSchema graphQLSchema) { + GraphQLInputType type = inputField.getType(); + GraphQLUnmodifiedType graphQLUnmodifiedType = GraphQLTypeUtil.unwrapAll(type); + Vertex typeVertex = assertNotNull(schemaGraph.getType(graphQLUnmodifiedType.getName())); + Edge typeEdge = new Edge(inputFieldVertex, typeVertex); + String typeEdgeLabel = "type=" + GraphQLTypeUtil.simplePrint(type) + ";defaultValue="; + if (inputField.hasSetDefaultValue()) { + typeEdgeLabel += AstPrinter.printAst(ValuesResolver.valueToLiteral(inputField.getInputFieldDefaultValue(), inputField.getType(), GraphQLContext.getDefault(), Locale.getDefault())); + } + + typeEdge.setLabel(typeEdgeLabel); + schemaGraph.addEdge(typeEdge); + } + + private void handleUnion(Vertex unionVertex, SchemaGraph schemaGraph, GraphQLSchema graphQLSchema) { + GraphQLUnionType unionType = (GraphQLUnionType) graphQLSchema.getType(unionVertex.get("name")); + List types = unionType.getTypes(); + for (GraphQLNamedOutputType unionMemberType : types) { + Vertex unionMemberVertex = assertNotNull(schemaGraph.getType(unionMemberType.getName())); + schemaGraph.addEdge(new Edge(unionVertex, unionMemberVertex)); + } + } + + private void handleInterfaceVertex(Vertex interfaceVertex, SchemaGraph schemaGraph, GraphQLSchema graphQLSchema) { + GraphQLInterfaceType interfaceType = (GraphQLInterfaceType) graphQLSchema.getType(interfaceVertex.get("name")); + + for (GraphQLNamedOutputType implementsInterface : interfaceType.getInterfaces()) { + Vertex implementsInterfaceVertex = assertNotNull(schemaGraph.getType(implementsInterface.getName())); + schemaGraph.addEdge(new Edge(interfaceVertex, implementsInterfaceVertex, "implements " + implementsInterface.getName())); + } + + List fieldDefinitions = interfaceType.getFieldDefinitions(); + for (GraphQLFieldDefinition fieldDefinition : fieldDefinitions) { + Vertex fieldVertex = schemaGraph.findTargetVertex(interfaceVertex, vertex -> vertex.getType().equals("Field") && + vertex.get("name").equals(fieldDefinition.getName())).get(); + handleField(fieldVertex, fieldDefinition, schemaGraph, graphQLSchema); + } + + } + + private void handleObjectVertex(Vertex objectVertex, SchemaGraph schemaGraph, GraphQLSchema graphQLSchema) { + GraphQLObjectType objectType = graphQLSchema.getObjectType(objectVertex.get("name")); + + for (GraphQLNamedOutputType implementsInterface : objectType.getInterfaces()) { + Vertex implementsInterfaceVertex = assertNotNull(schemaGraph.getType(implementsInterface.getName())); + schemaGraph.addEdge(new Edge(objectVertex, implementsInterfaceVertex, "implements " + implementsInterface.getName())); + } + + List fieldDefinitions = objectType.getFieldDefinitions(); + for (GraphQLFieldDefinition fieldDefinition : fieldDefinitions) { + Vertex fieldVertex = schemaGraph.findTargetVertex(objectVertex, vertex -> vertex.getType().equals("Field") && + vertex.get("name").equals(fieldDefinition.getName())).get(); + handleField(fieldVertex, fieldDefinition, schemaGraph, graphQLSchema); + } + } + + private void handleField(Vertex fieldVertex, GraphQLFieldDefinition fieldDefinition, SchemaGraph + schemaGraph, GraphQLSchema graphQLSchema) { + GraphQLOutputType type = fieldDefinition.getType(); + GraphQLUnmodifiedType graphQLUnmodifiedType = GraphQLTypeUtil.unwrapAll(type); + Vertex typeVertex = assertNotNull(schemaGraph.getType(graphQLUnmodifiedType.getName())); + Edge typeEdge = new Edge(fieldVertex, typeVertex); + typeEdge.setLabel("type=" + GraphQLTypeUtil.simplePrint(type) + ";"); + schemaGraph.addEdge(typeEdge); + + for (GraphQLArgument graphQLArgument : fieldDefinition.getArguments()) { + Vertex argumentVertex = schemaGraph.findTargetVertex(fieldVertex, vertex -> vertex.getType().equals("Argument") && + vertex.get("name").equals(graphQLArgument.getName())).get(); + handleArgument(argumentVertex, graphQLArgument, schemaGraph); + } + } + + private void handleDirective(Vertex directive, SchemaGraph schemaGraph, GraphQLSchema graphQLSchema) { + GraphQLDirective graphQLDirective = graphQLSchema.getDirective(directive.getName()); + for (GraphQLArgument graphQLArgument : graphQLDirective.getArguments()) { + Vertex argumentVertex = schemaGraph.findTargetVertex(directive, vertex -> vertex.isOfType(SchemaGraph.ARGUMENT) && + vertex.getName().equals(graphQLArgument.getName())).get(); + handleArgument(argumentVertex, graphQLArgument, schemaGraph); + } + + } + + private void handleArgument(Vertex argumentVertex, GraphQLArgument graphQLArgument, SchemaGraph schemaGraph) { + GraphQLInputType type = graphQLArgument.getType(); + GraphQLUnmodifiedType graphQLUnmodifiedType = GraphQLTypeUtil.unwrapAll(type); + Vertex typeVertex = assertNotNull(schemaGraph.getType(graphQLUnmodifiedType.getName())); + Edge typeEdge = new Edge(argumentVertex, typeVertex); + String typeEdgeLabel = "type=" + GraphQLTypeUtil.simplePrint(type) + ";defaultValue="; + if (graphQLArgument.hasSetDefaultValue()) { + typeEdgeLabel += AstPrinter.printAst(ValuesResolver.valueToLiteral(graphQLArgument.getArgumentDefaultValue(), graphQLArgument.getType(), GraphQLContext.getDefault(), Locale.getDefault())); + } + typeEdge.setLabel(typeEdgeLabel); + schemaGraph.addEdge(typeEdge); + } + + private void newObject(GraphQLObjectType graphQLObjectType, SchemaGraph schemaGraph, boolean isIntrospectionNode) { + Vertex objectVertex = new Vertex(SchemaGraph.OBJECT, debugPrefix + String.valueOf(counter++)); + objectVertex.setBuiltInType(isIntrospectionNode); + objectVertex.add("name", graphQLObjectType.getName()); + objectVertex.add("description", desc(graphQLObjectType.getDescription())); + for (GraphQLFieldDefinition fieldDefinition : graphQLObjectType.getFieldDefinitions()) { + Vertex newFieldVertex = newField(fieldDefinition, schemaGraph, isIntrospectionNode); + schemaGraph.addVertex(newFieldVertex); + schemaGraph.addEdge(new Edge(objectVertex, newFieldVertex)); + } + schemaGraph.addVertex(objectVertex); + schemaGraph.addType(graphQLObjectType.getName(), objectVertex); + createAppliedDirectives(objectVertex, graphQLObjectType.getDirectives(), schemaGraph); + } + + private Vertex newField(GraphQLFieldDefinition graphQLFieldDefinition, SchemaGraph schemaGraph, boolean isIntrospectionNode) { + Vertex fieldVertex = new Vertex(SchemaGraph.FIELD, debugPrefix + String.valueOf(counter++)); + fieldVertex.setBuiltInType(isIntrospectionNode); + fieldVertex.add("name", graphQLFieldDefinition.getName()); + fieldVertex.add("description", desc(graphQLFieldDefinition.getDescription())); + for (GraphQLArgument argument : graphQLFieldDefinition.getArguments()) { + Vertex argumentVertex = newArgument(argument, schemaGraph, isIntrospectionNode); + schemaGraph.addVertex(argumentVertex); + schemaGraph.addEdge(new Edge(fieldVertex, argumentVertex)); + } + createAppliedDirectives(fieldVertex, graphQLFieldDefinition.getDirectives(), schemaGraph); + return fieldVertex; + } + + private Vertex newArgument(GraphQLArgument graphQLArgument, SchemaGraph schemaGraph, boolean isIntrospectionNode) { + Vertex vertex = new Vertex(SchemaGraph.ARGUMENT, debugPrefix + String.valueOf(counter++)); + vertex.setBuiltInType(isIntrospectionNode); + vertex.add("name", graphQLArgument.getName()); + vertex.add("description", desc(graphQLArgument.getDescription())); + createAppliedDirectives(vertex, graphQLArgument.getDirectives(), schemaGraph); + return vertex; + } + + private void newScalar(GraphQLScalarType scalarType, SchemaGraph schemaGraph, boolean isIntrospectionNode) { + Vertex scalarVertex = new Vertex(SchemaGraph.SCALAR, debugPrefix + String.valueOf(counter++)); + scalarVertex.setBuiltInType(isIntrospectionNode); + if (ScalarInfo.isGraphqlSpecifiedScalar(scalarType.getName())) { + scalarVertex.setBuiltInType(true); + } + scalarVertex.add("name", scalarType.getName()); + scalarVertex.add("description", desc(scalarType.getDescription())); + scalarVertex.add("specifiedByUrl", scalarType.getSpecifiedByUrl()); + schemaGraph.addVertex(scalarVertex); + schemaGraph.addType(scalarType.getName(), scalarVertex); + createAppliedDirectives(scalarVertex, scalarType.getDirectives(), schemaGraph); + } + + private void newInterface(GraphQLInterfaceType interfaceType, SchemaGraph schemaGraph, boolean isIntrospectionNode) { + Vertex interfaceVertex = new Vertex(SchemaGraph.INTERFACE, debugPrefix + String.valueOf(counter++)); + interfaceVertex.setBuiltInType(isIntrospectionNode); + interfaceVertex.add("name", interfaceType.getName()); + interfaceVertex.add("description", desc(interfaceType.getDescription())); + for (GraphQLFieldDefinition fieldDefinition : interfaceType.getFieldDefinitions()) { + Vertex newFieldVertex = newField(fieldDefinition, schemaGraph, isIntrospectionNode); + schemaGraph.addVertex(newFieldVertex); + schemaGraph.addEdge(new Edge(interfaceVertex, newFieldVertex)); + } + schemaGraph.addVertex(interfaceVertex); + schemaGraph.addType(interfaceType.getName(), interfaceVertex); + createAppliedDirectives(interfaceVertex, interfaceType.getDirectives(), schemaGraph); + } + + private void newEnum(GraphQLEnumType enumType, SchemaGraph schemaGraph, boolean isIntrospectionNode) { + Vertex enumVertex = new Vertex(SchemaGraph.ENUM, debugPrefix + String.valueOf(counter++)); + enumVertex.setBuiltInType(isIntrospectionNode); + enumVertex.add("name", enumType.getName()); + enumVertex.add("description", desc(enumType.getDescription())); + for (GraphQLEnumValueDefinition enumValue : enumType.getValues()) { + Vertex enumValueVertex = new Vertex(SchemaGraph.ENUM_VALUE, debugPrefix + String.valueOf(counter++)); + enumValueVertex.setBuiltInType(isIntrospectionNode); + enumValueVertex.add("name", enumValue.getName()); + schemaGraph.addVertex(enumValueVertex); + schemaGraph.addEdge(new Edge(enumVertex, enumValueVertex)); + createAppliedDirectives(enumValueVertex, enumValue.getDirectives(), schemaGraph); + } + schemaGraph.addVertex(enumVertex); + schemaGraph.addType(enumType.getName(), enumVertex); + createAppliedDirectives(enumVertex, enumType.getDirectives(), schemaGraph); + } + + private void newUnion(GraphQLUnionType unionType, SchemaGraph schemaGraph, boolean isIntrospectionNode) { + Vertex unionVertex = new Vertex(SchemaGraph.UNION, debugPrefix + String.valueOf(counter++)); + unionVertex.setBuiltInType(isIntrospectionNode); + unionVertex.add("name", unionType.getName()); + unionVertex.add("description", desc(unionType.getDescription())); + schemaGraph.addVertex(unionVertex); + schemaGraph.addType(unionType.getName(), unionVertex); + createAppliedDirectives(unionVertex, unionType.getDirectives(), schemaGraph); + } + + private void newInputObject(GraphQLInputObjectType inputObject, SchemaGraph schemaGraph, boolean isIntrospectionNode) { + Vertex inputObjectVertex = new Vertex(SchemaGraph.INPUT_OBJECT, debugPrefix + String.valueOf(counter++)); + inputObjectVertex.setBuiltInType(isIntrospectionNode); + inputObjectVertex.add("name", inputObject.getName()); + inputObjectVertex.add("description", desc(inputObject.getDescription())); + for (GraphQLInputObjectField inputObjectField : inputObject.getFieldDefinitions()) { + Vertex newInputField = newInputField(inputObjectField, schemaGraph, isIntrospectionNode); + Edge newEdge = new Edge(inputObjectVertex, newInputField); + schemaGraph.addEdge(newEdge); + } + schemaGraph.addVertex(inputObjectVertex); + schemaGraph.addType(inputObject.getName(), inputObjectVertex); + createAppliedDirectives(inputObjectVertex, inputObject.getDirectives(), schemaGraph); + } + + private void createAppliedDirectives(Vertex from, + List appliedDirectives, + SchemaGraph schemaGraph) { + Map countByName = new LinkedHashMap<>(); + for (GraphQLDirective appliedDirective : appliedDirectives) { + Vertex appliedDirectiveVertex = new Vertex(SchemaGraph.APPLIED_DIRECTIVE, debugPrefix + String.valueOf(counter++)); + appliedDirectiveVertex.add("name", appliedDirective.getName()); + for (GraphQLArgument appliedArgument : appliedDirective.getArguments()) { + if (appliedArgument.hasSetValue()) { + Vertex appliedArgumentVertex = new Vertex(SchemaGraph.APPLIED_ARGUMENT, debugPrefix + String.valueOf(counter++)); + appliedArgumentVertex.add("name", appliedArgument.getName()); + appliedArgumentVertex.add("value", AstPrinter.printAst(ValuesResolver.valueToLiteral(appliedArgument.getArgumentValue(), appliedArgument.getType(), GraphQLContext.getDefault(), Locale.getDefault()))); + schemaGraph.addVertex(appliedArgumentVertex); + schemaGraph.addEdge(new Edge(appliedDirectiveVertex, appliedArgumentVertex)); + } + } + schemaGraph.addVertex(appliedDirectiveVertex); + + // repeatable directives means we can have multiple directives with the same name + // the edge label indicates the applied directive index + Integer count = countByName.getOrDefault(appliedDirective.getName(), 0); + schemaGraph.addEdge(new Edge(from, appliedDirectiveVertex, String.valueOf(count))); + countByName.put(appliedDirective.getName(), count + 1); + } + } + + private void newDirective(GraphQLDirective directive, SchemaGraph schemaGraph) { + Vertex directiveVertex = new Vertex(SchemaGraph.DIRECTIVE, debugPrefix + String.valueOf(counter++)); + directiveVertex.add("name", directive.getName()); + directiveVertex.add("repeatable", directive.isRepeatable()); + directiveVertex.add("locations", directive.validLocations()); + boolean graphqlSpecified = DirectiveInfo.isGraphqlSpecifiedDirective(directive.getName()); + directiveVertex.setBuiltInType(graphqlSpecified); + directiveVertex.add("description", desc(directive.getDescription())); + for (GraphQLArgument argument : directive.getArguments()) { + Vertex argumentVertex = newArgument(argument, schemaGraph, graphqlSpecified); + schemaGraph.addVertex(argumentVertex); + schemaGraph.addEdge(new Edge(directiveVertex, argumentVertex)); + } + schemaGraph.addDirective(directive.getName(), directiveVertex); + schemaGraph.addVertex(directiveVertex); + } + + private Vertex newInputField(GraphQLInputObjectField inputField, SchemaGraph schemaGraph, boolean isIntrospectionNode) { + Vertex vertex = new Vertex(SchemaGraph.INPUT_FIELD, debugPrefix + String.valueOf(counter++)); + schemaGraph.addVertex(vertex); + vertex.setBuiltInType(isIntrospectionNode); + vertex.add("name", inputField.getName()); + vertex.add("description", desc(inputField.getDescription())); + createAppliedDirectives(vertex, inputField.getDirectives(), schemaGraph); + return vertex; + } + + private String desc(String desc) { + return desc; +// return desc != null ? desc.replace("\n", "\\n") : null; + } + +} diff --git a/src/main/java/graphql/schema/diffing/SortSourceGraph.java b/src/main/java/graphql/schema/diffing/SortSourceGraph.java new file mode 100644 index 0000000000..d9169e167d --- /dev/null +++ b/src/main/java/graphql/schema/diffing/SortSourceGraph.java @@ -0,0 +1,141 @@ +package graphql.schema.diffing; + +import graphql.Internal; + +import java.util.ArrayList; +import java.util.Comparator; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import java.util.concurrent.atomic.AtomicInteger; + +@Internal +public class SortSourceGraph { + + public static void sortSourceGraph(SchemaGraph sourceGraph, SchemaGraph targetGraph, FillupIsolatedVertices.IsolatedVertices isolatedVertices) { +// // we sort descending by number of possible target vertices +// Collections.sort(sourceGraph.getVertices(), (v1, v2) -> +// +// { +// +// int v2Count = v2.isBuiltInType() ? -1 : (v2.isIsolated() ? 0 : isolatedVertices.possibleMappings.get(v2).size()); +// int v1Count = v1.isBuiltInType() ? -1 : (v1.isIsolated() ? 0 : isolatedVertices.possibleMappings.get(v1).size()); +// return Integer.compare(v2Count, v1Count); +// }); +// +// for (Vertex vertex : sourceGraph.getVertices()) { +// System.out.println("c: " + isolatedVertices.possibleMappings.get(vertex).size() + " v: " + vertex); +// } + +// +// +// // how often does each source edge (based on the label) appear in target graph + Map targetLabelCount = new LinkedHashMap<>(); + for (Edge targetEdge : targetGraph.getEdges()) { + targetLabelCount.computeIfAbsent(targetEdge.getLabel(), __ -> new AtomicInteger()).incrementAndGet(); + } + // how often does each source vertex (based on the data) appear in the target graph + Map targetVertexDataCount = new LinkedHashMap<>(); + for (Vertex targetVertex : targetGraph.getVertices()) { + targetVertexDataCount.computeIfAbsent(targetVertex.toData(), __ -> new AtomicInteger()).incrementAndGet(); + } + + // an infrequency weight is 1 - count in target. Meaning the higher the + // value, the smaller the count, the less frequent it. + // Higher Infrequency => more unique is the vertex/label + Map vertexInfrequencyWeights = new LinkedHashMap<>(); + Map edgesInfrequencyWeights = new LinkedHashMap<>(); + for (Vertex vertex : sourceGraph.getVertices()) { + vertexInfrequencyWeights.put(vertex, 1 - targetVertexDataCount.getOrDefault(vertex.toData(), new AtomicInteger()).get()); + } + for (Edge edge : sourceGraph.getEdges()) { + edgesInfrequencyWeights.put(edge, 1 - targetLabelCount.getOrDefault(edge.getLabel(), new AtomicInteger()).get()); + } + + /** + * vertices are sorted by increasing frequency/decreasing infrequency/decreasing uniqueness + * we start with the most unique/least frequent/most infrequent and add incrementally the next most infrequent. + */ + + //TODO: improve this: this is doing to much: we just want the max infrequent vertex, not all sorted + ArrayList nextCandidates = new ArrayList<>(sourceGraph.getVertices()); + nextCandidates.sort(Comparator.comparingInt(o -> totalInfrequencyWeightWithAdjacentEdges(sourceGraph, o, vertexInfrequencyWeights, edgesInfrequencyWeights))); + + Vertex curVertex = nextCandidates.get(nextCandidates.size() - 1); + nextCandidates.remove(nextCandidates.size() - 1); + + List result = new ArrayList<>(); + result.add(curVertex); + while (nextCandidates.size() > 0) { Vertex nextOne = null; + int curMaxWeight = Integer.MIN_VALUE; + int index = 0; + int nextOneIndex = -1; + + // which ones of the candidates has the highest infrequency weight relatively to the current result set of vertices + for (Vertex candidate : nextCandidates) { + List allAdjacentEdges = sourceGraph.getAllAdjacentEdges(result, candidate); + int totalWeight = totalInfrequencyWeightWithSomeEdges(candidate, allAdjacentEdges, vertexInfrequencyWeights, edgesInfrequencyWeights); + if (totalWeight > curMaxWeight) { + nextOne = candidate; + nextOneIndex = index; + curMaxWeight = totalWeight; + } + index++; + } + result.add(nextOne); + nextCandidates.remove(nextOneIndex); + } + sourceGraph.setVertices(result); + } + + + private static int totalInfrequencyWeightWithSomeEdges(Vertex vertex, + List edges, + Map vertexInfrequencyWeights, + Map edgesInfrequencyWeights) { + if (vertex.isBuiltInType()) { + return Integer.MIN_VALUE + 1; + } + if (vertex.isIsolated()) { + return Integer.MIN_VALUE + 2; + } + return vertexInfrequencyWeights.get(vertex) + edges.stream().mapToInt(edgesInfrequencyWeights::get).sum(); + } + + private static int totalInfrequencyWeightWithAdjacentEdges(SchemaGraph sourceGraph, + Vertex vertex, + Map vertexInfrequencyWeights, + Map edgesInfrequencyWeights) { + if (vertex.isBuiltInType()) { + return Integer.MIN_VALUE + 1; + } + if (vertex.isIsolated()) { + return Integer.MIN_VALUE + 2; + } + List adjacentEdges = sourceGraph.getAdjacentEdges(vertex); + return vertexInfrequencyWeights.get(vertex) + adjacentEdges.stream().mapToInt(edgesInfrequencyWeights::get).sum(); + } + + private int infrequencyWeightForVertex(Vertex sourceVertex, SchemaGraph targetGraph) { + int count = 0; + for (Vertex targetVertex : targetGraph.getVertices()) { + if (sourceVertex.isEqualTo(targetVertex)) { + count++; + } + } + return 1 - count; + } + + private int infrequencyWeightForEdge(Edge sourceEdge, SchemaGraph targetGraph) { + int count = 0; + for (Edge targetEdge : targetGraph.getEdges()) { + if (sourceEdge.isEqualTo(targetEdge)) { + count++; + } + } + return 1 - count; + } + + + +} diff --git a/src/main/java/graphql/schema/diffing/Util.java b/src/main/java/graphql/schema/diffing/Util.java new file mode 100644 index 0000000000..38bd0878c1 --- /dev/null +++ b/src/main/java/graphql/schema/diffing/Util.java @@ -0,0 +1,30 @@ +package graphql.schema.diffing; + +import graphql.Internal; + +import java.util.List; +import java.util.Set; + +@Internal +public class Util { + public static void diffNamedList(Set sourceNames, + Set targetNames, + List deleted, + List inserted, + List same) { + for (String sourceName : sourceNames) { + if (targetNames.contains(sourceName)) { + same.add(sourceName); + } else { + deleted.add(sourceName); + } + } + + for (String targetName : targetNames) { + if (!sourceNames.contains(targetName)) { + inserted.add(targetName); + } + } + } + +} diff --git a/src/main/java/graphql/schema/diffing/Vertex.java b/src/main/java/graphql/schema/diffing/Vertex.java new file mode 100644 index 0000000000..0011f63fe8 --- /dev/null +++ b/src/main/java/graphql/schema/diffing/Vertex.java @@ -0,0 +1,135 @@ +package graphql.schema.diffing; + +import graphql.Assert; +import graphql.Internal; + +import java.util.LinkedHashMap; +import java.util.LinkedHashSet; +import java.util.Map; +import java.util.Objects; +import java.util.Set; + +@Internal +public class Vertex { + + private String type; + private Map properties = new LinkedHashMap<>(); + private String debugName; + private boolean isolated; + + + private boolean builtInType; + + public static Vertex newIsolatedNode(String debugName) { + Vertex vertex = new Vertex(SchemaGraph.ISOLATED, debugName); + vertex.isolated = true; + return vertex; + } + + public static Set newIsolatedNodes(int count, String debugName) { + Set result = new LinkedHashSet<>(); + for (int i = 1; i <= count; i++) { + Vertex vertex = new Vertex(SchemaGraph.ISOLATED, debugName + i); + vertex.isolated = true; + result.add(vertex); + } + return result; + } + + public Vertex(String type, String debugName) { + this.type = type; + this.debugName = debugName; + } + + public boolean isIsolated() { + return isolated; + } + + public void add(String propName, Object propValue) { + properties.put(propName, propValue); + } + + public String getType() { + return type; + } + + public T get(String propName) { + return (T) properties.get(propName); + } + + public T getProperty(String name) { + return (T) properties.get(name); + } + + public String getName() { + return (String) Assert.assertNotNull(properties.get("name"), () -> String.format("should not call getName on %s", this)); + } + + public Map getProperties() { + return properties; + } + + public String getDebugName() { + return debugName; + } + + public boolean isOfType(String type) { + return this.type.equals(type); + } + + public boolean isEqualTo(Vertex other) { + return other != null && + Objects.equals(this.type, other.type) && + Objects.equals(this.properties, other.properties); + } + + public boolean isBuiltInType() { + return builtInType; + } + + public void setBuiltInType(boolean builtInType) { + this.builtInType = builtInType; + } + + @Override + public String toString() { + return "Vertex{" + + "type='" + type + '\'' + + ", properties=" + properties + + ", debugName='" + debugName + '\'' + + ", builtInType='" + builtInType + '\'' + + '}'; + } + + public VertexData toData() { + return new VertexData(this.type, this.properties); + } + + public static class VertexData { + private final String type; + private final Map properties; + + public VertexData(String type, Map properties) { + this.type = type; + this.properties = properties; + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + VertexData that = (VertexData) o; + return Objects.equals(type, that.type) && Objects.equals(properties, that.properties); + } + + @Override + public int hashCode() { + return Objects.hash(type, properties); + } + } + +} diff --git a/src/main/java/graphql/schema/diffing/ana/EditOperationAnalysisResult.java b/src/main/java/graphql/schema/diffing/ana/EditOperationAnalysisResult.java new file mode 100644 index 0000000000..d2eca0f7a4 --- /dev/null +++ b/src/main/java/graphql/schema/diffing/ana/EditOperationAnalysisResult.java @@ -0,0 +1,61 @@ +package graphql.schema.diffing.ana; + +import graphql.Internal; + +import java.util.Map; + +@Internal +public class EditOperationAnalysisResult { + private final Map objectDifferences; + private final Map interfaceDifferences; + private final Map unionDifferences; + private final Map enumDifferences; + private final Map inputObjectDifferences; + private final Map scalarDifferences; + + private final Map directiveDifferences; + + public EditOperationAnalysisResult(Map objectChanges, + Map interfaceDifferences, + Map unionDifferences, + Map enumDifferences, + Map inputObjectDifferences, + Map scalarDifferences, + Map directiveDifferences) { + this.objectDifferences = objectChanges; + this.interfaceDifferences = interfaceDifferences; + this.unionDifferences = unionDifferences; + this.enumDifferences = enumDifferences; + this.inputObjectDifferences = inputObjectDifferences; + this.scalarDifferences = scalarDifferences; + this.directiveDifferences = directiveDifferences; + } + + public Map getObjectDifferences() { + return objectDifferences; + } + + public Map getInterfaceDifferences() { + return interfaceDifferences; + } + + public Map getUnionDifferences() { + return unionDifferences; + } + + public Map getEnumDifferences() { + return enumDifferences; + } + + public Map getInputObjectDifferences() { + return inputObjectDifferences; + } + + public Map getScalarDifferences() { + return scalarDifferences; + } + + public Map getDirectiveDifferences() { + return directiveDifferences; + } +} diff --git a/src/main/java/graphql/schema/diffing/ana/EditOperationAnalyzer.java b/src/main/java/graphql/schema/diffing/ana/EditOperationAnalyzer.java new file mode 100644 index 0000000000..9fcc3ce154 --- /dev/null +++ b/src/main/java/graphql/schema/diffing/ana/EditOperationAnalyzer.java @@ -0,0 +1,1824 @@ +package graphql.schema.diffing.ana; + +import graphql.Assert; +import graphql.Internal; +import graphql.schema.GraphQLSchema; +import graphql.schema.diffing.Edge; +import graphql.schema.diffing.EditOperation; +import graphql.schema.diffing.Mapping; +import graphql.schema.diffing.SchemaGraph; +import graphql.schema.diffing.Vertex; +import graphql.schema.diffing.ana.SchemaDifference.AppliedDirectiveArgumentRename; +import graphql.schema.diffing.ana.SchemaDifference.AppliedDirectiveObjectLocation; +import graphql.schema.idl.ScalarInfo; + +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; + +import static graphql.Assert.assertTrue; +import static graphql.schema.diffing.ana.SchemaDifference.AppliedDirectiveAddition; +import static graphql.schema.diffing.ana.SchemaDifference.AppliedDirectiveArgumentDeletion; +import static graphql.schema.diffing.ana.SchemaDifference.AppliedDirectiveArgumentValueModification; +import static graphql.schema.diffing.ana.SchemaDifference.AppliedDirectiveDeletion; +import static graphql.schema.diffing.ana.SchemaDifference.AppliedDirectiveDirectiveArgumentLocation; +import static graphql.schema.diffing.ana.SchemaDifference.AppliedDirectiveEnumLocation; +import static graphql.schema.diffing.ana.SchemaDifference.AppliedDirectiveEnumValueLocation; +import static graphql.schema.diffing.ana.SchemaDifference.AppliedDirectiveInputObjectFieldLocation; +import static graphql.schema.diffing.ana.SchemaDifference.AppliedDirectiveInputObjectLocation; +import static graphql.schema.diffing.ana.SchemaDifference.AppliedDirectiveInterfaceFieldArgumentLocation; +import static graphql.schema.diffing.ana.SchemaDifference.AppliedDirectiveInterfaceFieldLocation; +import static graphql.schema.diffing.ana.SchemaDifference.AppliedDirectiveInterfaceLocation; +import static graphql.schema.diffing.ana.SchemaDifference.AppliedDirectiveObjectFieldArgumentLocation; +import static graphql.schema.diffing.ana.SchemaDifference.AppliedDirectiveObjectFieldLocation; +import static graphql.schema.diffing.ana.SchemaDifference.AppliedDirectiveScalarLocation; +import static graphql.schema.diffing.ana.SchemaDifference.AppliedDirectiveUnionLocation; +import static graphql.schema.diffing.ana.SchemaDifference.DirectiveAddition; +import static graphql.schema.diffing.ana.SchemaDifference.DirectiveArgumentAddition; +import static graphql.schema.diffing.ana.SchemaDifference.DirectiveArgumentDefaultValueModification; +import static graphql.schema.diffing.ana.SchemaDifference.DirectiveArgumentDeletion; +import static graphql.schema.diffing.ana.SchemaDifference.DirectiveArgumentRename; +import static graphql.schema.diffing.ana.SchemaDifference.DirectiveArgumentTypeModification; +import static graphql.schema.diffing.ana.SchemaDifference.DirectiveDeletion; +import static graphql.schema.diffing.ana.SchemaDifference.DirectiveDifference; +import static graphql.schema.diffing.ana.SchemaDifference.DirectiveModification; +import static graphql.schema.diffing.ana.SchemaDifference.EnumAddition; +import static graphql.schema.diffing.ana.SchemaDifference.EnumDeletion; +import static graphql.schema.diffing.ana.SchemaDifference.EnumDifference; +import static graphql.schema.diffing.ana.SchemaDifference.EnumModification; +import static graphql.schema.diffing.ana.SchemaDifference.EnumValueAddition; +import static graphql.schema.diffing.ana.SchemaDifference.EnumValueDeletion; +import static graphql.schema.diffing.ana.SchemaDifference.InputObjectAddition; +import static graphql.schema.diffing.ana.SchemaDifference.InputObjectDeletion; +import static graphql.schema.diffing.ana.SchemaDifference.InputObjectDifference; +import static graphql.schema.diffing.ana.SchemaDifference.InputObjectFieldAddition; +import static graphql.schema.diffing.ana.SchemaDifference.InputObjectFieldDefaultValueModification; +import static graphql.schema.diffing.ana.SchemaDifference.InputObjectFieldDeletion; +import static graphql.schema.diffing.ana.SchemaDifference.InputObjectFieldRename; +import static graphql.schema.diffing.ana.SchemaDifference.InputObjectFieldTypeModification; +import static graphql.schema.diffing.ana.SchemaDifference.InputObjectModification; +import static graphql.schema.diffing.ana.SchemaDifference.InterfaceAddition; +import static graphql.schema.diffing.ana.SchemaDifference.InterfaceDeletion; +import static graphql.schema.diffing.ana.SchemaDifference.InterfaceDifference; +import static graphql.schema.diffing.ana.SchemaDifference.InterfaceFieldAddition; +import static graphql.schema.diffing.ana.SchemaDifference.InterfaceFieldArgumentAddition; +import static graphql.schema.diffing.ana.SchemaDifference.InterfaceFieldArgumentDefaultValueModification; +import static graphql.schema.diffing.ana.SchemaDifference.InterfaceFieldArgumentDeletion; +import static graphql.schema.diffing.ana.SchemaDifference.InterfaceFieldArgumentRename; +import static graphql.schema.diffing.ana.SchemaDifference.InterfaceFieldArgumentTypeModification; +import static graphql.schema.diffing.ana.SchemaDifference.InterfaceFieldDeletion; +import static graphql.schema.diffing.ana.SchemaDifference.InterfaceFieldRename; +import static graphql.schema.diffing.ana.SchemaDifference.InterfaceFieldTypeModification; +import static graphql.schema.diffing.ana.SchemaDifference.InterfaceInterfaceImplementationAddition; +import static graphql.schema.diffing.ana.SchemaDifference.InterfaceInterfaceImplementationDeletion; +import static graphql.schema.diffing.ana.SchemaDifference.InterfaceModification; +import static graphql.schema.diffing.ana.SchemaDifference.ObjectAddition; +import static graphql.schema.diffing.ana.SchemaDifference.ObjectDeletion; +import static graphql.schema.diffing.ana.SchemaDifference.ObjectDifference; +import static graphql.schema.diffing.ana.SchemaDifference.ObjectFieldAddition; +import static graphql.schema.diffing.ana.SchemaDifference.ObjectFieldArgumentAddition; +import static graphql.schema.diffing.ana.SchemaDifference.ObjectFieldArgumentDefaultValueModification; +import static graphql.schema.diffing.ana.SchemaDifference.ObjectFieldArgumentDeletion; +import static graphql.schema.diffing.ana.SchemaDifference.ObjectFieldArgumentRename; +import static graphql.schema.diffing.ana.SchemaDifference.ObjectFieldArgumentTypeModification; +import static graphql.schema.diffing.ana.SchemaDifference.ObjectFieldDeletion; +import static graphql.schema.diffing.ana.SchemaDifference.ObjectFieldRename; +import static graphql.schema.diffing.ana.SchemaDifference.ObjectFieldTypeModification; +import static graphql.schema.diffing.ana.SchemaDifference.ObjectInterfaceImplementationAddition; +import static graphql.schema.diffing.ana.SchemaDifference.ObjectInterfaceImplementationDeletion; +import static graphql.schema.diffing.ana.SchemaDifference.ObjectModification; +import static graphql.schema.diffing.ana.SchemaDifference.ScalarAddition; +import static graphql.schema.diffing.ana.SchemaDifference.ScalarDeletion; +import static graphql.schema.diffing.ana.SchemaDifference.ScalarDifference; +import static graphql.schema.diffing.ana.SchemaDifference.ScalarModification; +import static graphql.schema.diffing.ana.SchemaDifference.UnionAddition; +import static graphql.schema.diffing.ana.SchemaDifference.UnionDeletion; +import static graphql.schema.diffing.ana.SchemaDifference.UnionDifference; +import static graphql.schema.diffing.ana.SchemaDifference.UnionMemberAddition; +import static graphql.schema.diffing.ana.SchemaDifference.UnionMemberDeletion; +import static graphql.schema.diffing.ana.SchemaDifference.UnionModification; + +/** + * Higher level GraphQL semantic assigned to + */ +@Internal +public class EditOperationAnalyzer { + + private GraphQLSchema oldSchema; + private GraphQLSchema newSchema; + private SchemaGraph oldSchemaGraph; + private SchemaGraph newSchemaGraph; + + private Map objectDifferences = new LinkedHashMap<>(); + private Map interfaceDifferences = new LinkedHashMap<>(); + private Map unionDifferences = new LinkedHashMap<>(); + private Map enumDifferences = new LinkedHashMap<>(); + private Map inputObjectDifferences = new LinkedHashMap<>(); + private Map scalarDifferences = new LinkedHashMap<>(); + + private Map directiveDifferences = new LinkedHashMap<>(); + + public EditOperationAnalyzer(GraphQLSchema oldSchema, + GraphQLSchema newSchema, + SchemaGraph oldSchemaGraph, + SchemaGraph newSchemaGraph + ) { + this.oldSchema = oldSchema; + this.newSchema = newSchema; + this.oldSchemaGraph = oldSchemaGraph; + this.newSchemaGraph = newSchemaGraph; + } + + public EditOperationAnalysisResult analyzeEdits(List editOperations, Mapping mapping) { + handleTypeVertexChanges(editOperations); + + for (EditOperation editOperation : editOperations) { + switch (editOperation.getOperation()) { + case CHANGE_VERTEX: + if (editOperation.getTargetVertex().isOfType(SchemaGraph.FIELD)) { + fieldChanged(editOperation); + } else if (editOperation.getTargetVertex().isOfType(SchemaGraph.ARGUMENT)) { + handleArgumentChange(editOperation); + } else if (editOperation.getTargetVertex().isOfType(SchemaGraph.INPUT_FIELD)) { + handleInputFieldChange(editOperation); + } + break; + case INSERT_VERTEX: + if (editOperation.getTargetVertex().isOfType(SchemaGraph.FIELD)) { + fieldAdded(editOperation); + } else if (editOperation.getTargetVertex().isOfType(SchemaGraph.ARGUMENT)) { + argumentAdded(editOperation); + } else if (editOperation.getTargetVertex().isOfType(SchemaGraph.INPUT_FIELD)) { + inputFieldAdded(editOperation); + } + break; + case DELETE_VERTEX: + if (editOperation.getSourceVertex().isOfType(SchemaGraph.ARGUMENT)) { + argumentDeleted(editOperation); + } else if (editOperation.getSourceVertex().isOfType(SchemaGraph.FIELD)) { + fieldDeleted(editOperation); + } else if (editOperation.getSourceVertex().isOfType(SchemaGraph.INPUT_FIELD)) { + inputFieldDeleted(editOperation); + } + } + } + handleTypeChanges(editOperations, mapping); + handleImplementsChanges(editOperations, mapping); + handleUnionMemberChanges(editOperations, mapping); + handleEnumValuesChanges(editOperations, mapping); + handleAppliedDirectives(editOperations, mapping); + + return new EditOperationAnalysisResult( + objectDifferences, + interfaceDifferences, + unionDifferences, + enumDifferences, + inputObjectDifferences, + scalarDifferences, + directiveDifferences); + } + + + private void handleAppliedDirectives(List editOperations, Mapping mapping) { + + for (EditOperation editOperation : editOperations) { + switch (editOperation.getOperation()) { + case INSERT_VERTEX: + if (editOperation.getTargetVertex().isOfType(SchemaGraph.APPLIED_DIRECTIVE)) { + appliedDirectiveAdded(editOperation); + } + break; + case CHANGE_VERTEX: + if (editOperation.getTargetVertex().isOfType(SchemaGraph.APPLIED_ARGUMENT)) { + appliedDirectiveArgumentChanged(editOperation); + } + break; + case DELETE_VERTEX: + if (editOperation.getSourceVertex().isOfType(SchemaGraph.APPLIED_DIRECTIVE)) { + appliedDirectiveDeleted(editOperation); + } else if (editOperation.getSourceVertex().isOfType(SchemaGraph.APPLIED_ARGUMENT)) { + appliedDirectiveArgumentDeleted(editOperation); + } + break; + + } + } + + } + + private void appliedDirectiveDeleted(EditOperation editOperation) { + Vertex appliedDirective = editOperation.getSourceVertex(); + Vertex container = oldSchemaGraph.getAppliedDirectiveContainerForAppliedDirective(appliedDirective); + if (container.isOfType(SchemaGraph.FIELD)) { + appliedDirectiveDeletedFromField(appliedDirective, container); + } else if (container.isOfType(SchemaGraph.ARGUMENT)) { + appliedDirectiveDeletedFromArgument(appliedDirective, container); + } else if (container.isOfType(SchemaGraph.OBJECT)) { + Vertex object = container; + if (isObjectDeleted(object.getName())) { + return; + } + AppliedDirectiveObjectLocation location = new AppliedDirectiveObjectLocation(object.getName()); + AppliedDirectiveDeletion appliedDirectiveDeletion = new AppliedDirectiveDeletion(location, appliedDirective.getName()); + getObjectModification(object.getName()).getDetails().add(appliedDirectiveDeletion); + } else if (container.isOfType(SchemaGraph.INTERFACE)) { + Vertex interfaze = container; + if (isInterfaceDeleted(interfaze.getName())) { + return; + } + AppliedDirectiveInterfaceLocation location = new AppliedDirectiveInterfaceLocation(interfaze.getName()); + AppliedDirectiveDeletion appliedDirectiveDeletion = new AppliedDirectiveDeletion(location, appliedDirective.getName()); + getInterfaceModification(interfaze.getName()).getDetails().add(appliedDirectiveDeletion); + } else if (container.isOfType(SchemaGraph.SCALAR)) { + Vertex scalar = container; + if (isScalarDeleted(scalar.getName())) { + return; + } + AppliedDirectiveScalarLocation location = new AppliedDirectiveScalarLocation(scalar.getName()); + AppliedDirectiveDeletion appliedDirectiveDeletion = new AppliedDirectiveDeletion(location, appliedDirective.getName()); + getScalarModification(scalar.getName()).getDetails().add(appliedDirectiveDeletion); + } else if (container.isOfType(SchemaGraph.ENUM)) { + Vertex enumVertex = container; + if (isEnumDeleted(enumVertex.getName())) { + return; + } + AppliedDirectiveEnumLocation location = new AppliedDirectiveEnumLocation(enumVertex.getName()); + AppliedDirectiveDeletion appliedDirectiveDeletion = new AppliedDirectiveDeletion(location, appliedDirective.getName()); + getEnumModification(enumVertex.getName()).getDetails().add(appliedDirectiveDeletion); + } else if (container.isOfType(SchemaGraph.ENUM_VALUE)) { + Vertex enumValue = container; + Vertex enumVertex = oldSchemaGraph.getEnumForEnumValue(enumValue); + if (isEnumDeleted(enumVertex.getName())) { + return; + } + if (isEnumValueDeletedFromExistingEnum(enumVertex.getName(), enumValue.getName())) { + return; + } + AppliedDirectiveEnumValueLocation location = new AppliedDirectiveEnumValueLocation(enumVertex.getName(), enumValue.getName()); + AppliedDirectiveDeletion appliedDirectiveDeletion = new AppliedDirectiveDeletion(location, appliedDirective.getName()); + getEnumModification(enumVertex.getName()).getDetails().add(appliedDirectiveDeletion); + } else if (container.isOfType(SchemaGraph.INPUT_OBJECT)) { + Vertex inputObject = container; + if (isInputObjectDeleted(inputObject.getName())) { + return; + } + AppliedDirectiveInputObjectLocation location = new AppliedDirectiveInputObjectLocation(inputObject.getName()); + AppliedDirectiveDeletion appliedDirectiveDeletion = new AppliedDirectiveDeletion(location, appliedDirective.getName()); + getInputObjectModification(inputObject.getName()).getDetails().add(appliedDirectiveDeletion); + } else if (container.isOfType(SchemaGraph.INPUT_FIELD)) { + Vertex inputField = container; + Vertex inputObject = oldSchemaGraph.getInputObjectForInputField(inputField); + if (isInputObjectDeleted(inputObject.getName())) { + return; + } + if (isInputFieldDeletedFromExistingInputObject(inputObject.getName(), inputField.getName())) { + return; + } + AppliedDirectiveInputObjectFieldLocation location = new AppliedDirectiveInputObjectFieldLocation(inputObject.getName(), inputField.getName()); + AppliedDirectiveDeletion appliedDirectiveDeletion = new AppliedDirectiveDeletion(location, appliedDirective.getName()); + getInputObjectModification(inputObject.getName()).getDetails().add(appliedDirectiveDeletion); + } else if (container.isOfType(SchemaGraph.UNION)) { + Vertex union = container; + if (isUnionDeleted(union.getName())) { + return; + } + AppliedDirectiveUnionLocation location = new AppliedDirectiveUnionLocation(union.getName()); + AppliedDirectiveDeletion appliedDirectiveDeletion = new AppliedDirectiveDeletion(location, appliedDirective.getName()); + getUnionModification(union.getName()).getDetails().add(appliedDirectiveDeletion); + } + } + + private void appliedDirectiveArgumentDeleted(EditOperation editOperation) { + Vertex deletedArgument = editOperation.getSourceVertex(); + Vertex appliedDirective = oldSchemaGraph.getAppliedDirectiveForAppliedArgument(deletedArgument); + Vertex container = oldSchemaGraph.getAppliedDirectiveContainerForAppliedDirective(appliedDirective); + + if (container.isOfType(SchemaGraph.FIELD)) { + Vertex field = container; + Vertex interfaceOrObjective = oldSchemaGraph.getFieldsContainerForField(field); + if (interfaceOrObjective.isOfType(SchemaGraph.OBJECT)) { + Vertex object = interfaceOrObjective; + AppliedDirectiveObjectFieldLocation location = new AppliedDirectiveObjectFieldLocation(object.getName(), field.getName()); + getObjectModification(object.getName()).getDetails().add(new AppliedDirectiveArgumentDeletion(location, deletedArgument.getName())); + } else { + assertTrue(interfaceOrObjective.isOfType(SchemaGraph.INTERFACE)); + Vertex interfaze = interfaceOrObjective; + AppliedDirectiveInterfaceFieldLocation location = new AppliedDirectiveInterfaceFieldLocation(interfaze.getName(), field.getName()); + getInterfaceModification(interfaze.getName()).getDetails().add(new AppliedDirectiveArgumentDeletion(location, deletedArgument.getName())); + } + } + + } + + private void appliedDirectiveArgumentChanged(EditOperation editOperation) { + Vertex appliedArgument = editOperation.getTargetVertex(); + String oldArgumentName = editOperation.getSourceVertex().getName(); + String newArgumentName = editOperation.getTargetVertex().getName(); + boolean nameChanged = !oldArgumentName.equals(newArgumentName); + + String oldValue = editOperation.getSourceVertex().get("value"); + String newValue = editOperation.getTargetVertex().get("value"); + boolean valueChanged = !oldValue.equals(newValue); + + + Vertex appliedDirective = newSchemaGraph.getAppliedDirectiveForAppliedArgument(appliedArgument); + Vertex container = newSchemaGraph.getAppliedDirectiveContainerForAppliedDirective(appliedDirective); + if (container.isOfType(SchemaGraph.FIELD)) { + Vertex field = container; + Vertex interfaceOrObjective = newSchemaGraph.getFieldsContainerForField(field); + if (interfaceOrObjective.isOfType(SchemaGraph.OBJECT)) { + Vertex object = interfaceOrObjective; + AppliedDirectiveObjectFieldLocation location = new AppliedDirectiveObjectFieldLocation(object.getName(), field.getName()); + if (valueChanged) { + AppliedDirectiveArgumentValueModification argumentValueModification = new AppliedDirectiveArgumentValueModification(location, newArgumentName, oldValue, newValue); + getObjectModification(object.getName()).getDetails().add(argumentValueModification); + } + if (nameChanged) { + AppliedDirectiveArgumentRename argumentRename = new AppliedDirectiveArgumentRename(location, oldArgumentName, newArgumentName); + getObjectModification(object.getName()).getDetails().add(argumentRename); + + } + } + } + + } + + private void appliedDirectiveAdded(EditOperation editOperation) { + Vertex appliedDirective = editOperation.getTargetVertex(); + Vertex container = newSchemaGraph.getAppliedDirectiveContainerForAppliedDirective(appliedDirective); + if (container.isOfType(SchemaGraph.FIELD)) { + appliedDirectiveAddedToField(appliedDirective, container); + } else if (container.isOfType(SchemaGraph.ARGUMENT)) { + appliedDirectiveAddedToArgument(appliedDirective, container); + + } else if (container.isOfType(SchemaGraph.OBJECT)) { + Vertex object = container; + if (isObjectAdded(object.getName())) { + return; + } + AppliedDirectiveObjectLocation location = new AppliedDirectiveObjectLocation(object.getName()); + AppliedDirectiveAddition appliedDirectiveAddition = new AppliedDirectiveAddition(location, appliedDirective.getName()); + getObjectModification(object.getName()).getDetails().add(appliedDirectiveAddition); + } else if (container.isOfType(SchemaGraph.INTERFACE)) { + Vertex interfaze = container; + if (isInterfaceAdded(interfaze.getName())) { + return; + } + AppliedDirectiveInterfaceLocation location = new AppliedDirectiveInterfaceLocation(interfaze.getName()); + AppliedDirectiveAddition appliedDirectiveAddition = new AppliedDirectiveAddition(location, appliedDirective.getName()); + getInterfaceModification(interfaze.getName()).getDetails().add(appliedDirectiveAddition); + } else if (container.isOfType(SchemaGraph.SCALAR)) { + Vertex scalar = container; + if (isScalarAdded(scalar.getName())) { + return; + } + AppliedDirectiveScalarLocation location = new AppliedDirectiveScalarLocation(scalar.getName()); + AppliedDirectiveAddition appliedDirectiveAddition = new AppliedDirectiveAddition(location, appliedDirective.getName()); + getScalarModification(scalar.getName()).getDetails().add(appliedDirectiveAddition); + } else if (container.isOfType(SchemaGraph.ENUM)) { + Vertex enumVertex = container; + if (isEnumAdded(enumVertex.getName())) { + return; + } + AppliedDirectiveEnumLocation location = new AppliedDirectiveEnumLocation(enumVertex.getName()); + AppliedDirectiveAddition appliedDirectiveAddition = new AppliedDirectiveAddition(location, appliedDirective.getName()); + getEnumModification(enumVertex.getName()).getDetails().add(appliedDirectiveAddition); + } else if (container.isOfType(SchemaGraph.ENUM_VALUE)) { + Vertex enumValue = container; + Vertex enumVertex = newSchemaGraph.getEnumForEnumValue(enumValue); + if (isEnumAdded(enumVertex.getName())) { + return; + } + if (isNewEnumValueForExistingEnum(enumVertex.getName(), enumValue.getName())) { + return; + } + AppliedDirectiveEnumValueLocation location = new AppliedDirectiveEnumValueLocation(enumVertex.getName(), enumValue.getName()); + AppliedDirectiveAddition appliedDirectiveAddition = new AppliedDirectiveAddition(location, appliedDirective.getName()); + getEnumModification(enumVertex.getName()).getDetails().add(appliedDirectiveAddition); + } else if (container.isOfType(SchemaGraph.INPUT_OBJECT)) { + Vertex inputObject = container; + if (isInputObjectAdded(inputObject.getName())) { + return; + } + AppliedDirectiveInputObjectLocation location = new AppliedDirectiveInputObjectLocation(inputObject.getName()); + AppliedDirectiveAddition appliedDirectiveAddition = new AppliedDirectiveAddition(location, appliedDirective.getName()); + getInputObjectModification(inputObject.getName()).getDetails().add(appliedDirectiveAddition); + } else if (container.isOfType(SchemaGraph.INPUT_FIELD)) { + Vertex inputField = container; + Vertex inputObject = newSchemaGraph.getInputObjectForInputField(inputField); + if (isInputObjectAdded(inputObject.getName())) { + return; + } + if (isNewInputFieldExistingInputObject(inputObject.getName(), inputField.getName())) { + return; + } + AppliedDirectiveInputObjectFieldLocation location = new AppliedDirectiveInputObjectFieldLocation(inputObject.getName(), inputField.getName()); + AppliedDirectiveAddition appliedDirectiveAddition = new AppliedDirectiveAddition(location, appliedDirective.getName()); + getInputObjectModification(inputObject.getName()).getDetails().add(appliedDirectiveAddition); + } else if (container.isOfType(SchemaGraph.UNION)) { + Vertex union = container; + if (isUnionAdded(union.getName())) { + return; + } + AppliedDirectiveUnionLocation location = new AppliedDirectiveUnionLocation(union.getName()); + AppliedDirectiveAddition appliedDirectiveAddition = new AppliedDirectiveAddition(location, appliedDirective.getName()); + getUnionModification(union.getName()).getDetails().add(appliedDirectiveAddition); + } + } + + private void appliedDirectiveDeletedFromField(Vertex appliedDirective, Vertex container) { + Vertex field = container; + Vertex interfaceOrObjective = oldSchemaGraph.getFieldsContainerForField(field); + if (interfaceOrObjective.isOfType(SchemaGraph.OBJECT)) { + Vertex object = interfaceOrObjective; + + if (isObjectDeleted(object.getName())) { + return; + } + if (isFieldDeletedFromExistingObject(object.getName(), field.getName())) { + return; + } + AppliedDirectiveObjectFieldLocation location = new AppliedDirectiveObjectFieldLocation(object.getName(), field.getName()); + AppliedDirectiveDeletion appliedDirectiveDeletion = new AppliedDirectiveDeletion(location, appliedDirective.getName()); + getObjectModification(object.getName()).getDetails().add(appliedDirectiveDeletion); + } + } + + private void appliedDirectiveAddedToField(Vertex appliedDirective, Vertex container) { + Vertex field = container; + Vertex interfaceOrObjective = newSchemaGraph.getFieldsContainerForField(field); + if (interfaceOrObjective.isOfType(SchemaGraph.OBJECT)) { + Vertex object = interfaceOrObjective; + + if (isObjectAdded(object.getName())) { + return; + } + if (isFieldNewForExistingObject(object.getName(), field.getName())) { + return; + } + AppliedDirectiveObjectFieldLocation location = new AppliedDirectiveObjectFieldLocation(object.getName(), field.getName()); + AppliedDirectiveAddition appliedDirectiveAddition = new AppliedDirectiveAddition(location, appliedDirective.getName()); + getObjectModification(object.getName()).getDetails().add(appliedDirectiveAddition); + } + } + + private void appliedDirectiveDeletedFromArgument(Vertex appliedDirective, Vertex container) { + Vertex argument = container; + Vertex fieldOrDirective = oldSchemaGraph.getFieldOrDirectiveForArgument(argument); + if (fieldOrDirective.isOfType(SchemaGraph.FIELD)) { + Vertex field = fieldOrDirective; + Vertex interfaceOrObjective = oldSchemaGraph.getFieldsContainerForField(field); + if (interfaceOrObjective.isOfType(SchemaGraph.OBJECT)) { + Vertex object = interfaceOrObjective; + if (isObjectDeleted(object.getName())) { + return; + } + if (isFieldDeletedFromExistingObject(object.getName(), field.getName())) { + return; + } + if (isArgumentDeletedFromExistingObjectField(object.getName(), field.getName(), argument.getName())) { + return; + } + AppliedDirectiveObjectFieldArgumentLocation location = new AppliedDirectiveObjectFieldArgumentLocation(object.getName(), field.getName(), argument.getName()); + AppliedDirectiveDeletion appliedDirectiveDeletion = new AppliedDirectiveDeletion(location, appliedDirective.getName()); + getObjectModification(object.getName()).getDetails().add(appliedDirectiveDeletion); + } else { + assertTrue(interfaceOrObjective.isOfType(SchemaGraph.INTERFACE)); + Vertex interfaze = interfaceOrObjective; + if (isInterfaceDeleted(interfaze.getName())) { + return; + } + if (isFieldDeletedFromExistingInterface(interfaze.getName(), field.getName())) { + return; + } + if (isArgumentDeletedFromExistingInterfaceField(interfaze.getName(), field.getName(), argument.getName())) { + return; + } + AppliedDirectiveInterfaceFieldArgumentLocation location = new AppliedDirectiveInterfaceFieldArgumentLocation(interfaze.getName(), field.getName(), argument.getName()); + AppliedDirectiveDeletion appliedDirectiveDeletion = new AppliedDirectiveDeletion(location, appliedDirective.getName()); + getInterfaceModification(interfaze.getName()).getDetails().add(appliedDirectiveDeletion); + } + } else { + assertTrue(fieldOrDirective.isOfType(SchemaGraph.DIRECTIVE)); + Vertex directive = fieldOrDirective; + if (isDirectiveDeleted(directive.getName())) { + return; + } + if (isArgumentDeletedFromExistingDirective(directive.getName(), argument.getName())) { + return; + } + AppliedDirectiveDirectiveArgumentLocation location = new AppliedDirectiveDirectiveArgumentLocation(directive.getName(), argument.getName()); + AppliedDirectiveDeletion appliedDirectiveDeletion = new AppliedDirectiveDeletion(location, appliedDirective.getName()); + getDirectiveModification(directive.getName()).getDetails().add(appliedDirectiveDeletion); + } + } + + private void appliedDirectiveAddedToArgument(Vertex appliedDirective, Vertex container) { + Vertex argument = container; + Vertex fieldOrDirective = newSchemaGraph.getFieldOrDirectiveForArgument(argument); + if (fieldOrDirective.isOfType(SchemaGraph.FIELD)) { + Vertex field = fieldOrDirective; + Vertex interfaceOrObjective = newSchemaGraph.getFieldsContainerForField(field); + if (interfaceOrObjective.isOfType(SchemaGraph.OBJECT)) { + Vertex object = interfaceOrObjective; + if (isObjectAdded(object.getName())) { + return; + } + if (isFieldNewForExistingObject(object.getName(), field.getName())) { + return; + } + if (isArgumentNewForExistingObjectField(object.getName(), field.getName(), argument.getName())) { + return; + } + AppliedDirectiveObjectFieldArgumentLocation location = new AppliedDirectiveObjectFieldArgumentLocation(object.getName(), field.getName(), argument.getName()); + AppliedDirectiveAddition appliedDirectiveAddition = new AppliedDirectiveAddition(location, appliedDirective.getName()); + getObjectModification(object.getName()).getDetails().add(appliedDirectiveAddition); + } else { + assertTrue(interfaceOrObjective.isOfType(SchemaGraph.INTERFACE)); + Vertex interfaze = interfaceOrObjective; + if (isInterfaceAdded(interfaze.getName())) { + return; + } + if (isFieldNewForExistingInterface(interfaze.getName(), field.getName())) { + return; + } + if (isArgumentNewForExistingInterfaceField(interfaze.getName(), field.getName(), argument.getName())) { + return; + } + AppliedDirectiveInterfaceFieldArgumentLocation location = new AppliedDirectiveInterfaceFieldArgumentLocation(interfaze.getName(), field.getName(), argument.getName()); + AppliedDirectiveAddition appliedDirectiveAddition = new AppliedDirectiveAddition(location, appliedDirective.getName()); + getInterfaceModification(interfaze.getName()).getDetails().add(appliedDirectiveAddition); + } + } else { + assertTrue(fieldOrDirective.isOfType(SchemaGraph.DIRECTIVE)); + Vertex directive = fieldOrDirective; + if (isDirectiveAdded(directive.getName())) { + return; + } + if (isArgumentNewForExistingDirective(directive.getName(), argument.getName())) { + return; + } + AppliedDirectiveDirectiveArgumentLocation location = new AppliedDirectiveDirectiveArgumentLocation(directive.getName(), argument.getName()); + AppliedDirectiveAddition appliedDirectiveAddition = new AppliedDirectiveAddition(location, appliedDirective.getName()); + getDirectiveModification(directive.getName()).getDetails().add(appliedDirectiveAddition); + } + } + + private void handleTypeChanges(List editOperations, Mapping mapping) { + for (EditOperation editOperation : editOperations) { + Edge newEdge = editOperation.getTargetEdge(); + switch (editOperation.getOperation()) { + case INSERT_EDGE: + if (newEdge.getLabel().startsWith("type=")) { + typeEdgeInserted(editOperation, editOperations, mapping); + } + break; + case CHANGE_EDGE: + if (newEdge.getLabel().startsWith("type=")) { + typeEdgeChanged(editOperation); + } + break; + } + } + } + + private void handleUnionMemberChanges(List editOperations, Mapping mapping) { + for (EditOperation editOperation : editOperations) { + switch (editOperation.getOperation()) { + case INSERT_EDGE: + Edge newEdge = editOperation.getTargetEdge(); + if (newEdge.getFrom().isOfType(SchemaGraph.UNION)) { + handleUnionMemberAdded(editOperation); + } + break; + case DELETE_EDGE: + Edge oldEdge = editOperation.getSourceEdge(); + if (oldEdge.getFrom().isOfType(SchemaGraph.UNION)) { + handleUnionMemberDeleted(editOperation); + } + break; + } + } + } + + private void handleEnumValuesChanges(List editOperations, Mapping mapping) { + for (EditOperation editOperation : editOperations) { + switch (editOperation.getOperation()) { + case INSERT_EDGE: + Edge newEdge = editOperation.getTargetEdge(); + if (newEdge.getFrom().isOfType(SchemaGraph.ENUM)) { + handleEnumValueAdded(editOperation); + } + break; + case DELETE_EDGE: + Edge oldEdge = editOperation.getSourceEdge(); + if (oldEdge.getFrom().isOfType(SchemaGraph.ENUM)) { + handleEnumValueDeleted(editOperation); + } + break; + } + } + } + + private void handleInputFieldChange(EditOperation editOperation) { + Vertex inputField = editOperation.getTargetVertex(); + Vertex inputObject = newSchemaGraph.getInputObjectForInputField(inputField); + String oldName = editOperation.getSourceVertex().getName(); + String newName = inputObject.getName(); + getInputObjectModification(newName).getDetails().add(new InputObjectFieldRename(oldName, inputField.getName())); + } + + private void handleArgumentChange(EditOperation editOperation) { + Vertex argument = editOperation.getTargetVertex(); + Vertex fieldOrDirective = newSchemaGraph.getFieldOrDirectiveForArgument(argument); + if (fieldOrDirective.isOfType(SchemaGraph.DIRECTIVE)) { + Vertex directive = fieldOrDirective; + DirectiveModification directiveModification = getDirectiveModification(directive.getName()); + String oldName = editOperation.getSourceVertex().getName(); + String newName = argument.getName(); + directiveModification.getDetails().add(new DirectiveArgumentRename(oldName, newName)); + + } else { + assertTrue(fieldOrDirective.isOfType(SchemaGraph.FIELD)); + Vertex field = fieldOrDirective; + Vertex fieldsContainerForField = newSchemaGraph.getFieldsContainerForField(field); + if (fieldsContainerForField.isOfType(SchemaGraph.OBJECT)) { + Vertex object = fieldsContainerForField; + ObjectModification objectModification = getObjectModification(object.getName()); + String oldName = editOperation.getSourceVertex().getName(); + String newName = argument.getName(); + objectModification.getDetails().add(new ObjectFieldArgumentRename(oldName, newName)); + } else { + assertTrue(fieldsContainerForField.isOfType(SchemaGraph.INTERFACE)); + Vertex interfaze = fieldsContainerForField; + InterfaceModification interfaceModification = getInterfaceModification(interfaze.getName()); + String oldName = editOperation.getSourceVertex().getName(); + String newName = argument.getName(); + interfaceModification.getDetails().add(new InterfaceFieldArgumentRename(oldName, newName)); + + } + } + } + + private void handleImplementsChanges(List editOperations, Mapping mapping) { + for (EditOperation editOperation : editOperations) { + switch (editOperation.getOperation()) { + case INSERT_EDGE: + Edge newEdge = editOperation.getTargetEdge(); + if (newEdge.getLabel().startsWith("implements ")) { + newInterfaceAddedToInterfaceOrObject(newEdge); + } + break; + case DELETE_EDGE: + Edge oldEdge = editOperation.getSourceEdge(); + if (oldEdge.getLabel().startsWith("implements ")) { + interfaceImplementationDeleted(oldEdge); + } + break; + } + } + } + + private void handleUnionMemberAdded(EditOperation editOperation) { + Edge newEdge = editOperation.getTargetEdge(); + Vertex union = newEdge.getFrom(); + if (isUnionAdded(union.getName())) { + return; + } + Vertex newMemberObject = newEdge.getTo(); + UnionModification unionModification = getUnionModification(union.getName()); + unionModification.getDetails().add(new UnionMemberAddition(newMemberObject.getName())); + } + + private void handleUnionMemberDeleted(EditOperation editOperation) { + Edge deletedEdge = editOperation.getSourceEdge(); + Vertex union = deletedEdge.getFrom(); + if (isUnionDeleted(union.getName())) { + return; + } + Vertex memberObject = deletedEdge.getTo(); + UnionModification unionModification = getUnionModification(union.getName()); + unionModification.getDetails().add(new UnionMemberDeletion(memberObject.getName())); + } + + private void handleEnumValueAdded(EditOperation editOperation) { + Edge newEdge = editOperation.getTargetEdge(); + Vertex enumVertex = newEdge.getFrom(); + if (isEnumAdded(enumVertex.getName())) { + return; + } + Vertex newValue = newEdge.getTo(); + EnumModification enumModification = getEnumModification(enumVertex.getName()); + enumModification.getDetails().add(new EnumValueAddition(newValue.getName())); + } + + private void handleEnumValueDeleted(EditOperation editOperation) { + Edge deletedEdge = editOperation.getSourceEdge(); + Vertex enumVertex = deletedEdge.getFrom(); + if (isEnumDeleted(enumVertex.getName())) { + return; + } + Vertex value = deletedEdge.getTo(); + EnumModification enumModification = getEnumModification(enumVertex.getName()); + enumModification.getDetails().add(new EnumValueDeletion(value.getName())); + } + + + private void fieldChanged(EditOperation editOperation) { + Vertex field = editOperation.getTargetVertex(); + Vertex fieldsContainerForField = newSchemaGraph.getFieldsContainerForField(field); + if (fieldsContainerForField.isOfType(SchemaGraph.OBJECT)) { + Vertex object = fieldsContainerForField; + ObjectModification objectModification = getObjectModification(object.getName()); + String oldName = editOperation.getSourceVertex().getName(); + String newName = field.getName(); + objectModification.getDetails().add(new ObjectFieldRename(oldName, newName)); + } else { + assertTrue(fieldsContainerForField.isOfType(SchemaGraph.INTERFACE)); + Vertex interfaze = fieldsContainerForField; + InterfaceModification interfaceModification = getInterfaceModification(interfaze.getName()); + String oldName = editOperation.getSourceVertex().getName(); + String newName = field.getName(); + interfaceModification.getDetails().add(new InterfaceFieldRename(oldName, newName)); + } + } + + private void inputFieldAdded(EditOperation editOperation) { + Vertex inputField = editOperation.getTargetVertex(); + Vertex inputObject = newSchemaGraph.getInputObjectForInputField(inputField); + if (isInputObjectAdded(inputObject.getName())) { + return; + } + InputObjectModification modification = getInputObjectModification(inputObject.getName()); + modification.getDetails().add(new InputObjectFieldAddition(inputField.getName())); + } + + private void fieldAdded(EditOperation editOperation) { + Vertex field = editOperation.getTargetVertex(); + Vertex fieldsContainerForField = newSchemaGraph.getFieldsContainerForField(field); + if (fieldsContainerForField.isOfType(SchemaGraph.OBJECT)) { + Vertex object = fieldsContainerForField; + if (isObjectAdded(object.getName())) { + return; + } + ObjectModification objectModification = getObjectModification(object.getName()); + String name = field.getName(); + objectModification.getDetails().add(new ObjectFieldAddition(name)); + } else { + assertTrue(fieldsContainerForField.isOfType(SchemaGraph.INTERFACE)); + Vertex interfaze = fieldsContainerForField; + if (isInterfaceAdded(interfaze.getName())) { + return; + } + InterfaceModification interfaceModification = getInterfaceModification(interfaze.getName()); + String name = field.getName(); + interfaceModification.getDetails().add(new InterfaceFieldAddition(name)); + } + } + + private void inputFieldDeleted(EditOperation editOperation) { + Vertex inputField = editOperation.getSourceVertex(); + Vertex inputObject = oldSchemaGraph.getInputObjectForInputField(inputField); + if (isInputObjectDeleted(inputObject.getName())) { + return; + } + getInputObjectModification(inputObject.getName()).getDetails().add(new InputObjectFieldDeletion(inputField.getName())); + } + + private void fieldDeleted(EditOperation editOperation) { + Vertex deletedField = editOperation.getSourceVertex(); + Vertex fieldsContainerForField = oldSchemaGraph.getFieldsContainerForField(deletedField); + if (fieldsContainerForField.isOfType(SchemaGraph.OBJECT)) { + Vertex object = fieldsContainerForField; + if (isObjectDeleted(object.getName())) { + return; + } + ObjectModification objectModification = getObjectModification(object.getName()); + String name = deletedField.getName(); + objectModification.getDetails().add(new ObjectFieldDeletion(name)); + } else { + assertTrue(fieldsContainerForField.isOfType(SchemaGraph.INTERFACE)); + Vertex interfaze = fieldsContainerForField; + if (isInterfaceDeleted(interfaze.getName())) { + return; + } + InterfaceModification interfaceModification = getInterfaceModification(interfaze.getName()); + String name = deletedField.getName(); + interfaceModification.getDetails().add(new InterfaceFieldDeletion(name)); + } + } + + + private void handleTypeVertexChanges(List editOperations) { + for (EditOperation editOperation : editOperations) { + switch (editOperation.getOperation()) { + case INSERT_VERTEX: + insertedTypeVertex(editOperation); + break; + case DELETE_VERTEX: + deletedTypeVertex(editOperation); + break; + case CHANGE_VERTEX: + changedTypeVertex(editOperation); + break; + } + } + } + + private void insertedTypeVertex(EditOperation editOperation) { + switch (editOperation.getTargetVertex().getType()) { + case SchemaGraph.OBJECT: + addedObject(editOperation); + break; + case SchemaGraph.INTERFACE: + addedInterface(editOperation); + break; + case SchemaGraph.UNION: + addedUnion(editOperation); + break; + case SchemaGraph.INPUT_OBJECT: + addedInputObject(editOperation); + break; + case SchemaGraph.ENUM: + addedEnum(editOperation); + break; + case SchemaGraph.SCALAR: + addedScalar(editOperation); + break; + case SchemaGraph.DIRECTIVE: + addedDirective(editOperation); + break; + } + + } + + private void deletedTypeVertex(EditOperation editOperation) { + switch (editOperation.getSourceVertex().getType()) { + case SchemaGraph.OBJECT: + removedObject(editOperation); + break; + case SchemaGraph.INTERFACE: + removedInterface(editOperation); + break; + case SchemaGraph.UNION: + removedUnion(editOperation); + break; + case SchemaGraph.INPUT_OBJECT: + removedInputObject(editOperation); + break; + case SchemaGraph.ENUM: + removedEnum(editOperation); + break; + case SchemaGraph.SCALAR: + deletedScalar(editOperation); + break; + case SchemaGraph.DIRECTIVE: + deletedDirective(editOperation); + break; + } + } + + private void changedTypeVertex(EditOperation editOperation) { + switch (editOperation.getTargetVertex().getType()) { + case SchemaGraph.OBJECT: + changedObject(editOperation); + break; + case SchemaGraph.INTERFACE: + changedInterface(editOperation); + break; + case SchemaGraph.UNION: + changedUnion(editOperation); + break; + case SchemaGraph.INPUT_OBJECT: + changedInputObject(editOperation); + break; + case SchemaGraph.ENUM: + changedEnum(editOperation); + break; + case SchemaGraph.SCALAR: + changedScalar(editOperation); + break; + case SchemaGraph.DIRECTIVE: + changedDirective(editOperation); + break; + } + + } + + + private void typeEdgeInserted(EditOperation editOperation, List editOperations, Mapping + mapping) { + Edge newEdge = editOperation.getTargetEdge(); + Vertex from = newEdge.getFrom(); + if (from.isOfType(SchemaGraph.FIELD)) { + typeEdgeInsertedForField(editOperation, editOperations, mapping); + } else if (from.isOfType(SchemaGraph.ARGUMENT)) { + typeEdgeInsertedForArgument(editOperation, editOperations, mapping); + } else if (from.isOfType(SchemaGraph.INPUT_FIELD)) { + typeEdgeInsertedForInputField(editOperation, editOperations, mapping); + } + + } + + private void typeEdgeInsertedForInputField(EditOperation + editOperation, List editOperations, Mapping mapping) { + Vertex inputField = editOperation.getTargetEdge().getFrom(); + Vertex inputObject = newSchemaGraph.getInputObjectForInputField(inputField); + if (isInputObjectAdded(inputObject.getName())) { + return; + } + if (isNewInputFieldExistingInputObject(inputObject.getName(), inputField.getName())) { + return; + } + String newType = getTypeFromEdgeLabel(editOperation.getTargetEdge()); + EditOperation deletedTypeEdgeOperation = findDeletedEdge(inputField, editOperations, mapping); + String oldType = getTypeFromEdgeLabel(deletedTypeEdgeOperation.getSourceEdge()); + InputObjectFieldTypeModification inputObjectFieldTypeModification = new InputObjectFieldTypeModification(inputField.getName(), oldType, newType); + getInputObjectModification(inputObject.getName()).getDetails().add(inputObjectFieldTypeModification); + } + + private void typeEdgeInsertedForArgument(EditOperation + editOperation, List editOperations, Mapping mapping) { + Vertex argument = editOperation.getTargetEdge().getFrom(); + Vertex fieldOrDirective = newSchemaGraph.getFieldOrDirectiveForArgument(argument); + if (fieldOrDirective.isOfType(SchemaGraph.FIELD)) { + Vertex field = fieldOrDirective; + Vertex objectOrInterface = newSchemaGraph.getFieldsContainerForField(field); + + if (objectOrInterface.isOfType(SchemaGraph.OBJECT)) { + Vertex object = objectOrInterface; + // if the whole object is new we are done + if (isObjectAdded(object.getName())) { + return; + } + // if the field is new, we are done too + if (isFieldNewForExistingObject(object.getName(), field.getName())) { + return; + } + // if the argument is new, we are done too + if (isArgumentNewForExistingObjectField(object.getName(), field.getName(), argument.getName())) { + return; + } + String newType = getTypeFromEdgeLabel(editOperation.getTargetEdge()); + // this means we have an existing object changed its type + // and there must be a deleted edge with the old type information + EditOperation deletedTypeEdgeOperation = findDeletedEdge(argument, editOperations, mapping); + String oldType = getTypeFromEdgeLabel(deletedTypeEdgeOperation.getSourceEdge()); + ObjectFieldArgumentTypeModification objectFieldArgumentTypeModification = new ObjectFieldArgumentTypeModification(field.getName(), argument.getName(), oldType, newType); + getObjectModification(object.getName()).getDetails().add(objectFieldArgumentTypeModification); + } else { + assertTrue(objectOrInterface.isOfType(SchemaGraph.INTERFACE)); + Vertex interfaze = objectOrInterface; + // if the whole object is new we are done + if (isInterfaceAdded(interfaze.getName())) { + return; + } + // if the field is new, we are done too + if (isFieldNewForExistingInterface(interfaze.getName(), field.getName())) { + return; + } + // if the argument is new, we are done too + if (isArgumentNewForExistingInterfaceField(interfaze.getName(), field.getName(), argument.getName())) { + return; + } + String newType = getTypeFromEdgeLabel(editOperation.getTargetEdge()); + // this means we have an existing object changed its type + // and there must be a deleted edge with the old type information + EditOperation deletedTypeEdgeOperation = findDeletedEdge(argument, editOperations, mapping); + String oldType = getTypeFromEdgeLabel(deletedTypeEdgeOperation.getSourceEdge()); + InterfaceFieldArgumentTypeModification interfaceFieldArgumentTypeModification = new InterfaceFieldArgumentTypeModification(field.getName(), argument.getName(), oldType, newType); + getInterfaceModification(interfaze.getName()).getDetails().add(interfaceFieldArgumentTypeModification); + } + } else { + assertTrue(fieldOrDirective.isOfType(SchemaGraph.DIRECTIVE)); + Vertex directive = fieldOrDirective; + if (isDirectiveAdded(directive.getName())) { + return; + } + if (isArgumentNewForExistingDirective(directive.getName(), argument.getName())) { + return; + } + String newType = getTypeFromEdgeLabel(editOperation.getTargetEdge()); + EditOperation deletedTypeEdgeOperation = findDeletedEdge(argument, editOperations, mapping); + String oldType = getTypeFromEdgeLabel(deletedTypeEdgeOperation.getSourceEdge()); + DirectiveArgumentTypeModification directiveArgumentTypeModification = new DirectiveArgumentTypeModification(argument.getName(), oldType, newType); + getDirectiveModification(directive.getName()).getDetails().add(directiveArgumentTypeModification); + } + + } + + private void typeEdgeInsertedForField(EditOperation + editOperation, List editOperations, Mapping mapping) { + Vertex field = editOperation.getTargetEdge().getFrom(); + Vertex objectOrInterface = newSchemaGraph.getFieldsContainerForField(field); + if (objectOrInterface.isOfType(SchemaGraph.OBJECT)) { + Vertex object = objectOrInterface; + // if the whole object is new we are done + if (isObjectAdded(object.getName())) { + return; + } + // if the field is new, we are done too + if (isFieldNewForExistingObject(object.getName(), field.getName())) { + return; + } + String newType = getTypeFromEdgeLabel(editOperation.getTargetEdge()); + // this means we have an existing object changed its type + // and there must be a deleted edge with the old type information + EditOperation deletedTypeEdgeOperation = findDeletedEdge(field, editOperations, mapping); + String oldType = getTypeFromEdgeLabel(deletedTypeEdgeOperation.getSourceEdge()); + ObjectFieldTypeModification objectFieldTypeModification = new ObjectFieldTypeModification(field.getName(), oldType, newType); + getObjectModification(object.getName()).getDetails().add(objectFieldTypeModification); + + } else { + assertTrue(objectOrInterface.isOfType(SchemaGraph.INTERFACE)); + Vertex interfaze = objectOrInterface; + if (isInterfaceAdded(interfaze.getName())) { + return; + } + if (isFieldNewForExistingInterface(interfaze.getName(), field.getName())) { + return; + } + String newType = getTypeFromEdgeLabel(editOperation.getTargetEdge()); + // this means we have an existing object changed its type + // and there must be a deleted edge with the old type information + EditOperation deletedTypeEdgeOperation = findDeletedEdge(field, editOperations, mapping); + String oldType = getTypeFromEdgeLabel(deletedTypeEdgeOperation.getSourceEdge()); + InterfaceFieldTypeModification interfaceFieldTypeModification = new InterfaceFieldTypeModification(field.getName(), oldType, newType); + getInterfaceModification(interfaze.getName()).getDetails().add(interfaceFieldTypeModification); + + } + } + + + private EditOperation findDeletedEdge(Vertex targetVertexFrom, List editOperations, Mapping + mapping) { + Vertex sourceVertexFrom = mapping.getSource(targetVertexFrom); + for (EditOperation editOperation : editOperations) { + if (editOperation.getOperation() == EditOperation.Operation.DELETE_EDGE) { + Edge deletedEdge = editOperation.getSourceEdge(); + if (deletedEdge.getFrom() == sourceVertexFrom) { + return editOperation; + } + } + } + return Assert.assertShouldNeverHappen(); + } + + + private void typeEdgeChanged(EditOperation editOperation) { + Edge targetEdge = editOperation.getTargetEdge(); + Vertex from = targetEdge.getFrom(); + if (from.isOfType(SchemaGraph.FIELD)) { + outputFieldTypeChanged(editOperation); + } else if (from.isOfType(SchemaGraph.ARGUMENT)) { + argumentTypeOrDefaultValueChanged(editOperation); + } else if (from.isOfType(SchemaGraph.INPUT_FIELD)) { + inputFieldTypeOrDefaultValueChanged(editOperation); + } + } + + private void inputFieldTypeOrDefaultValueChanged(EditOperation editOperation) { + Edge targetEdge = editOperation.getTargetEdge(); + Vertex inputField = targetEdge.getFrom(); + Vertex inputObject = newSchemaGraph.getInputObjectForInputField(inputField); + String oldDefaultValue = getDefaultValueFromEdgeLabel(editOperation.getSourceEdge()); + String newDefaultValue = getDefaultValueFromEdgeLabel(editOperation.getTargetEdge()); + if (!oldDefaultValue.equals(newDefaultValue)) { + InputObjectFieldDefaultValueModification modification = new InputObjectFieldDefaultValueModification(inputField.getName(), oldDefaultValue, newDefaultValue); + getInputObjectModification(inputObject.getName()).getDetails().add(modification); + } + String oldType = getTypeFromEdgeLabel(editOperation.getSourceEdge()); + String newType = getTypeFromEdgeLabel(editOperation.getTargetEdge()); + if (!oldType.equals(newType)) { + InputObjectFieldTypeModification inputObjectFieldTypeModification = new InputObjectFieldTypeModification(inputField.getName(), oldType, newType); + getInputObjectModification(inputObject.getName()).getDetails().add(inputObjectFieldTypeModification); + } + } + + private void argumentTypeOrDefaultValueChanged(EditOperation editOperation) { + Edge targetEdge = editOperation.getTargetEdge(); + Vertex argument = targetEdge.getFrom(); + Vertex fieldOrDirective = newSchemaGraph.getFieldOrDirectiveForArgument(argument); + if (fieldOrDirective.isOfType(SchemaGraph.FIELD)) { + Vertex field = fieldOrDirective; + Vertex objectOrInterface = newSchemaGraph.getFieldsContainerForField(field); + + String oldDefaultValue = getDefaultValueFromEdgeLabel(editOperation.getSourceEdge()); + String newDefaultValue = getDefaultValueFromEdgeLabel(editOperation.getTargetEdge()); + if (!oldDefaultValue.equals(newDefaultValue)) { + if (objectOrInterface.isOfType(SchemaGraph.OBJECT)) { + ObjectFieldArgumentDefaultValueModification defaultValueModification = new ObjectFieldArgumentDefaultValueModification( + field.getName(), + argument.getName(), + oldDefaultValue, + newDefaultValue); + getObjectModification(objectOrInterface.getName()).getDetails().add(defaultValueModification); + } else { + assertTrue(objectOrInterface.isOfType(SchemaGraph.INTERFACE)); + InterfaceFieldArgumentDefaultValueModification defaultValueModification = new InterfaceFieldArgumentDefaultValueModification( + field.getName(), + argument.getName(), + oldDefaultValue, + newDefaultValue); + getInterfaceModification(objectOrInterface.getName()).getDetails().add(defaultValueModification); + } + } + + String oldType = getTypeFromEdgeLabel(editOperation.getSourceEdge()); + String newType = getTypeFromEdgeLabel(editOperation.getTargetEdge()); + if (!oldType.equals(newType)) { + if (objectOrInterface.isOfType(SchemaGraph.OBJECT)) { + ObjectFieldArgumentTypeModification objectFieldArgumentTypeModification = new ObjectFieldArgumentTypeModification(field.getName(), argument.getName(), oldType, newType); + getObjectModification(objectOrInterface.getName()).getDetails().add(objectFieldArgumentTypeModification); + } else { + assertTrue(objectOrInterface.isOfType(SchemaGraph.INTERFACE)); + InterfaceFieldArgumentTypeModification interfaceFieldArgumentTypeModification = new InterfaceFieldArgumentTypeModification(field.getName(), argument.getName(), oldType, newType); + getInterfaceModification(objectOrInterface.getName()).getDetails().add(interfaceFieldArgumentTypeModification); + } + } + + } else { + assertTrue(fieldOrDirective.isOfType(SchemaGraph.DIRECTIVE)); + Vertex directive = fieldOrDirective; + + String oldDefaultValue = getDefaultValueFromEdgeLabel(editOperation.getSourceEdge()); + String newDefaultValue = getDefaultValueFromEdgeLabel(editOperation.getTargetEdge()); + if (!oldDefaultValue.equals(newDefaultValue)) { + getDirectiveModification(directive.getName()).getDetails().add(new DirectiveArgumentDefaultValueModification(argument.getName(), oldDefaultValue, newDefaultValue)); + } + + String oldType = getTypeFromEdgeLabel(editOperation.getSourceEdge()); + String newType = getTypeFromEdgeLabel(editOperation.getTargetEdge()); + + if (!oldType.equals(newType)) { + getDirectiveModification(directive.getName()).getDetails().add(new DirectiveArgumentTypeModification(argument.getName(), oldType, newType)); + + } + } + + } + + private void outputFieldTypeChanged(EditOperation editOperation) { + Edge targetEdge = editOperation.getTargetEdge(); + Vertex field = targetEdge.getFrom(); + Vertex container = newSchemaGraph.getFieldsContainerForField(field); + if (container.isOfType(SchemaGraph.OBJECT)) { + Vertex object = container; + ObjectModification objectModification = getObjectModification(object.getName()); + String fieldName = field.getName(); + String oldType = getTypeFromEdgeLabel(editOperation.getSourceEdge()); + String newType = getTypeFromEdgeLabel(editOperation.getTargetEdge()); + objectModification.getDetails().add(new ObjectFieldTypeModification(fieldName, oldType, newType)); + } else { + assertTrue(container.isOfType(SchemaGraph.INTERFACE)); + Vertex interfaze = container; + InterfaceModification interfaceModification = getInterfaceModification(interfaze.getName()); + String fieldName = field.getName(); + String oldType = getTypeFromEdgeLabel(editOperation.getSourceEdge()); + String newType = getTypeFromEdgeLabel(editOperation.getTargetEdge()); + interfaceModification.getDetails().add(new InterfaceFieldTypeModification(fieldName, oldType, newType)); + + } + + + } + + // TODO: this is not great, we should avoid parsing the label like that + private String getTypeFromEdgeLabel(Edge edge) { + String label = edge.getLabel(); + assertTrue(label.startsWith("type=")); + String type = label.substring("type=".length(), label.indexOf(";")); + return type; + } + + private String getDefaultValueFromEdgeLabel(Edge edge) { + String label = edge.getLabel(); + assertTrue(label.startsWith("type=")); + String defaultValue = label.substring(label.indexOf(";defaultValue=") + ";defaultValue=".length()); + return defaultValue; + } + + + private void interfaceImplementationDeleted(Edge deletedEdge) { + Vertex from = deletedEdge.getFrom(); + if (from.isOfType(SchemaGraph.OBJECT)) { + if (isObjectDeleted(from.getName())) { + return; + } + Vertex objectVertex = deletedEdge.getFrom(); + Vertex interfaceVertex = deletedEdge.getTo(); + ObjectInterfaceImplementationDeletion deletion = new ObjectInterfaceImplementationDeletion(interfaceVertex.getName()); + getObjectModification(objectVertex.getName()).getDetails().add(deletion); + + } else { + assertTrue(from.isOfType(SchemaGraph.INTERFACE)); + if (isInterfaceDeleted(from.getName())) { + return; + } + Vertex interfaceFromVertex = deletedEdge.getFrom(); + Vertex interfaceVertex = deletedEdge.getTo(); + InterfaceInterfaceImplementationDeletion deletion = new InterfaceInterfaceImplementationDeletion(interfaceVertex.getName()); + getInterfaceModification(interfaceFromVertex.getName()).getDetails().add(deletion); + } + + } + + private void newInterfaceAddedToInterfaceOrObject(Edge newEdge) { + Vertex from = newEdge.getFrom(); + if (from.isOfType(SchemaGraph.OBJECT)) { + if (isObjectAdded(from.getName())) { + return; + } + Vertex objectVertex = newEdge.getFrom(); + Vertex interfaceVertex = newEdge.getTo(); + ObjectInterfaceImplementationAddition objectInterfaceImplementationAddition = new ObjectInterfaceImplementationAddition(interfaceVertex.getName()); + getObjectModification(objectVertex.getName()).getDetails().add(objectInterfaceImplementationAddition); + + } else { + assertTrue(from.isOfType(SchemaGraph.INTERFACE)); + if (isInterfaceAdded(from.getName())) { + return; + } + Vertex interfaceFromVertex = newEdge.getFrom(); + Vertex interfaceVertex = newEdge.getTo(); + InterfaceInterfaceImplementationAddition addition = new InterfaceInterfaceImplementationAddition(interfaceVertex.getName()); + getInterfaceModification(interfaceFromVertex.getName()).getDetails().add(addition); + } + + } + + private boolean isDirectiveAdded(String name) { + return directiveDifferences.containsKey(name) && directiveDifferences.get(name) instanceof DirectiveAddition; + } + + private boolean isDirectiveDeleted(String name) { + return directiveDifferences.containsKey(name) && directiveDifferences.get(name) instanceof DirectiveDeletion; + } + + private boolean isObjectAdded(String name) { + return objectDifferences.containsKey(name) && objectDifferences.get(name) instanceof ObjectAddition; + } + + private boolean isUnionAdded(String name) { + return unionDifferences.containsKey(name) && unionDifferences.get(name) instanceof UnionAddition; + } + + private boolean isUnionDeleted(String name) { + return unionDifferences.containsKey(name) && unionDifferences.get(name) instanceof UnionDeletion; + } + + private boolean isEnumDeleted(String name) { + return enumDifferences.containsKey(name) && enumDifferences.get(name) instanceof EnumDeletion; + } + + private boolean isEnumAdded(String name) { + return enumDifferences.containsKey(name) && enumDifferences.get(name) instanceof EnumAddition; + } + + private boolean isInputObjectAdded(String name) { + return inputObjectDifferences.containsKey(name) && inputObjectDifferences.get(name) instanceof InputObjectAddition; + } + + private boolean isInputObjectDeleted(String name) { + return inputObjectDifferences.containsKey(name) && inputObjectDifferences.get(name) instanceof InputObjectDeletion; + } + + private boolean isInputFieldAdded(String name) { + return inputObjectDifferences.containsKey(name) && inputObjectDifferences.get(name) instanceof InputObjectAddition; + } + + private boolean isNewInputFieldExistingInputObject(String inputObjectName, String fieldName) { + if (!inputObjectDifferences.containsKey(inputObjectName)) { + return false; + } + if (!(inputObjectDifferences.get(inputObjectName) instanceof InputObjectModification)) { + return false; + } + InputObjectModification modification = (InputObjectModification) inputObjectDifferences.get(inputObjectName); + List newFields = modification.getDetails(InputObjectFieldAddition.class); + return newFields.stream().anyMatch(detail -> detail.getName().equals(fieldName)); + } + + private boolean isInputFieldDeletedFromExistingInputObject(String inputObjectName, String fieldName) { + if (!inputObjectDifferences.containsKey(inputObjectName)) { + return false; + } + if (!(inputObjectDifferences.get(inputObjectName) instanceof InputObjectModification)) { + return false; + } + InputObjectModification modification = (InputObjectModification) inputObjectDifferences.get(inputObjectName); + List deletedFields = modification.getDetails(InputObjectFieldDeletion.class); + return deletedFields.stream().anyMatch(detail -> detail.getName().equals(fieldName)); + } + + private boolean isArgumentNewForExistingDirective(String directiveName, String argumentName) { + if (!directiveDifferences.containsKey(directiveName)) { + return false; + } + if (!(directiveDifferences.get(directiveName) instanceof DirectiveModification)) { + return false; + } + DirectiveModification directiveModification = (DirectiveModification) directiveDifferences.get(directiveName); + List newArgs = directiveModification.getDetails(DirectiveArgumentAddition.class); + return newArgs.stream().anyMatch(detail -> detail.getName().equals(argumentName)); + } + + private boolean isArgumentDeletedFromExistingDirective(String directiveName, String argumentName) { + if (!directiveDifferences.containsKey(directiveName)) { + return false; + } + if (!(directiveDifferences.get(directiveName) instanceof DirectiveModification)) { + return false; + } + DirectiveModification directiveModification = (DirectiveModification) directiveDifferences.get(directiveName); + List deletedArgs = directiveModification.getDetails(DirectiveArgumentDeletion.class); + return deletedArgs.stream().anyMatch(detail -> detail.getName().equals(argumentName)); + } + + private boolean isArgumentNewForExistingObjectField(String objectName, String fieldName, String argumentName) { + if (!objectDifferences.containsKey(objectName)) { + return false; + } + if (!(objectDifferences.get(objectName) instanceof ObjectModification)) { + return false; + } + // finding out if the field was just added + ObjectModification objectModification = (ObjectModification) objectDifferences.get(objectName); + List newFields = objectModification.getDetails(ObjectFieldAddition.class); + boolean newField = newFields.stream().anyMatch(detail -> detail.getName().equals(fieldName)); + if (newField) { + return false; + } + // now finding out if the argument is new + List newArgs = objectModification.getDetails(ObjectFieldArgumentAddition.class); + return newArgs.stream().anyMatch(detail -> detail.getFieldName().equals(fieldName) && detail.getName().equals(argumentName)); + } + + private boolean isArgumentDeletedFromExistingObjectField(String objectName, String fieldName, String argumentName) { + if (!objectDifferences.containsKey(objectName)) { + return false; + } + if (!(objectDifferences.get(objectName) instanceof ObjectModification)) { + return false; + } + // finding out if the field was just added + ObjectModification objectModification = (ObjectModification) objectDifferences.get(objectName); + List deletedFields = objectModification.getDetails(ObjectFieldDeletion.class); + boolean deletedField = deletedFields.stream().anyMatch(detail -> detail.getName().equals(fieldName)); + if (deletedField) { + return false; + } + // now finding out if the argument is deleted + List deletedArgs = objectModification.getDetails(ObjectFieldArgumentDeletion.class); + return deletedArgs.stream().anyMatch(detail -> detail.getFieldName().equals(fieldName) && detail.getName().equals(argumentName)); + } + + private boolean isArgumentDeletedFromExistingInterfaceField(String interfaceName, String fieldName, String argumentName) { + if (!interfaceDifferences.containsKey(interfaceName)) { + return false; + } + if (!(interfaceDifferences.get(interfaceName) instanceof InterfaceModification)) { + return false; + } + // finding out if the field was just added + InterfaceModification interfaceModification = (InterfaceModification) interfaceDifferences.get(interfaceName); + List deletedFields = interfaceModification.getDetails(InterfaceFieldDeletion.class); + boolean deletedField = deletedFields.stream().anyMatch(detail -> detail.getName().equals(fieldName)); + if (deletedField) { + return false; + } + // now finding out if the argument is deleted + List deletedArgs = interfaceModification.getDetails(InterfaceFieldArgumentDeletion.class); + return deletedArgs.stream().anyMatch(detail -> detail.getFieldName().equals(fieldName) && detail.getName().equals(argumentName)); + } + + private boolean isArgumentNewForExistingInterfaceField(String objectName, String fieldName, String argumentName) { + if (!interfaceDifferences.containsKey(objectName)) { + return false; + } + if (!(interfaceDifferences.get(objectName) instanceof InterfaceModification)) { + return false; + } + // finding out if the field was just added + InterfaceModification interfaceModification = (InterfaceModification) interfaceDifferences.get(objectName); + List newFields = interfaceModification.getDetails(InterfaceFieldAddition.class); + boolean newField = newFields.stream().anyMatch(detail -> detail.getName().equals(fieldName)); + if (newField) { + return false; + } + // now finding out if the argument is new + List newArgs = interfaceModification.getDetails(InterfaceFieldArgumentAddition.class); + return newArgs.stream().anyMatch(detail -> detail.getFieldName().equals(fieldName) && detail.getName().equals(argumentName)); + } + + private boolean isFieldNewForExistingObject(String objectName, String fieldName) { + if (!objectDifferences.containsKey(objectName)) { + return false; + } + if (!(objectDifferences.get(objectName) instanceof ObjectModification)) { + return false; + } + ObjectModification objectModification = (ObjectModification) objectDifferences.get(objectName); + List newFields = objectModification.getDetails(ObjectFieldAddition.class); + return newFields.stream().anyMatch(detail -> detail.getName().equals(fieldName)); + } + + private boolean isFieldDeletedFromExistingInterface(String interfaceName, String fieldName) { + if (!interfaceDifferences.containsKey(interfaceName)) { + return false; + } + if (!(interfaceDifferences.get(interfaceName) instanceof InterfaceModification)) { + return false; + } + InterfaceModification interfaceModification = (InterfaceModification) interfaceDifferences.get(interfaceName); + List deletedFields = interfaceModification.getDetails(InterfaceFieldDeletion.class); + return deletedFields.stream().anyMatch(detail -> detail.getName().equals(fieldName)); + } + + private boolean isFieldDeletedFromExistingObject(String objectName, String fieldName) { + if (!objectDifferences.containsKey(objectName)) { + return false; + } + if (!(objectDifferences.get(objectName) instanceof ObjectModification)) { + return false; + } + ObjectModification objectModification = (ObjectModification) objectDifferences.get(objectName); + List deletedFields = objectModification.getDetails(ObjectFieldDeletion.class); + return deletedFields.stream().anyMatch(detail -> detail.getName().equals(fieldName)); + } + + private boolean isNewEnumValueForExistingEnum(String enumName, String valueName) { + if (!enumDifferences.containsKey(enumName)) { + return false; + } + if (!(enumDifferences.get(enumName) instanceof EnumModification)) { + return false; + } + EnumModification enumModification = (EnumModification) enumDifferences.get(enumName); + List newValues = enumModification.getDetails(EnumValueAddition.class); + return newValues.stream().anyMatch(detail -> detail.getName().equals(valueName)); + } + + private boolean isEnumValueDeletedFromExistingEnum(String enumName, String valueName) { + if (!enumDifferences.containsKey(enumName)) { + return false; + } + if (!(enumDifferences.get(enumName) instanceof EnumModification)) { + return false; + } + EnumModification enumModification = (EnumModification) enumDifferences.get(enumName); + List deletedValues = enumModification.getDetails(EnumValueDeletion.class); + return deletedValues.stream().anyMatch(detail -> detail.getName().equals(valueName)); + } + + private boolean isFieldNewForExistingInterface(String interfaceName, String fieldName) { + if (!interfaceDifferences.containsKey(interfaceName)) { + return false; + } + if (!(interfaceDifferences.get(interfaceName) instanceof InterfaceModification)) { + return false; + } + InterfaceModification interfaceModification = (InterfaceModification) interfaceDifferences.get(interfaceName); + List newFields = interfaceModification.getDetails(InterfaceFieldAddition.class); + return newFields.stream().anyMatch(detail -> detail.getName().equals(fieldName)); + } + + private boolean isObjectDeleted(String name) { + return objectDifferences.containsKey(name) && objectDifferences.get(name) instanceof ObjectDeletion; + } + + private boolean isInterfaceDeleted(String name) { + return interfaceDifferences.containsKey(name) && interfaceDifferences.get(name) instanceof InterfaceDeletion; + } + + private boolean isInterfaceAdded(String name) { + return interfaceDifferences.containsKey(name) && interfaceDifferences.get(name) instanceof InterfaceAddition; + } + + private boolean isScalarAdded(String name) { + return scalarDifferences.containsKey(name) && scalarDifferences.get(name) instanceof ScalarAddition; + } + + private boolean isScalarDeleted(String name) { + return scalarDifferences.containsKey(name) && scalarDifferences.get(name) instanceof ScalarDeletion; + } + + private ObjectModification getObjectModification(String newName) { + if (!objectDifferences.containsKey(newName)) { + objectDifferences.put(newName, new ObjectModification(newName)); + } + assertTrue(objectDifferences.get(newName) instanceof ObjectModification); + return (ObjectModification) objectDifferences.get(newName); + } + + private UnionModification getUnionModification(String newName) { + if (!unionDifferences.containsKey(newName)) { + unionDifferences.put(newName, new UnionModification(newName)); + } + assertTrue(unionDifferences.get(newName) instanceof UnionModification); + return (UnionModification) unionDifferences.get(newName); + } + + private EnumModification getEnumModification(String newName) { + if (!enumDifferences.containsKey(newName)) { + enumDifferences.put(newName, new EnumModification(newName)); + } + assertTrue(enumDifferences.get(newName) instanceof EnumModification); + return (EnumModification) enumDifferences.get(newName); + } + + private InputObjectModification getInputObjectModification(String newName) { + if (!inputObjectDifferences.containsKey(newName)) { + inputObjectDifferences.put(newName, new InputObjectModification(newName)); + } + assertTrue(inputObjectDifferences.get(newName) instanceof InputObjectModification); + return (InputObjectModification) inputObjectDifferences.get(newName); + } + + private DirectiveModification getDirectiveModification(String newName) { + if (!directiveDifferences.containsKey(newName)) { + directiveDifferences.put(newName, new DirectiveModification(newName)); + } + assertTrue(directiveDifferences.get(newName) instanceof DirectiveModification); + return (DirectiveModification) directiveDifferences.get(newName); + } + + private InterfaceModification getInterfaceModification(String newName) { + if (!interfaceDifferences.containsKey(newName)) { + interfaceDifferences.put(newName, new InterfaceModification(newName)); + } + assertTrue(interfaceDifferences.get(newName) instanceof InterfaceModification); + return (InterfaceModification) interfaceDifferences.get(newName); + } + + private ScalarModification getScalarModification(String newName) { + if (!scalarDifferences.containsKey(newName)) { + scalarDifferences.put(newName, new ScalarModification(newName)); + } + assertTrue(scalarDifferences.get(newName) instanceof ScalarModification); + return (ScalarModification) scalarDifferences.get(newName); + } + + + private void addedObject(EditOperation editOperation) { + String objectName = editOperation.getTargetVertex().getName(); + ObjectAddition objectAddition = new ObjectAddition(objectName); + objectDifferences.put(objectName, objectAddition); + } + + private void addedInterface(EditOperation editOperation) { + String objectName = editOperation.getTargetVertex().getName(); + + InterfaceAddition interfaceAddition = new InterfaceAddition(objectName); + interfaceDifferences.put(objectName, interfaceAddition); + } + + private void addedUnion(EditOperation editOperation) { + String unionName = editOperation.getTargetVertex().getName(); + + UnionAddition addition = new UnionAddition(unionName); + unionDifferences.put(unionName, addition); + } + + private void addedInputObject(EditOperation editOperation) { + String inputObjectName = editOperation.getTargetVertex().getName(); + + InputObjectAddition addition = new InputObjectAddition(inputObjectName); + inputObjectDifferences.put(inputObjectName, addition); + } + + private void addedEnum(EditOperation editOperation) { + String enumName = editOperation.getTargetVertex().getName(); + + EnumAddition enumAddition = new EnumAddition(enumName); + enumDifferences.put(enumName, enumAddition); + } + + private void addedScalar(EditOperation editOperation) { + String scalarName = editOperation.getTargetVertex().getName(); + // build in scalars can appear as added when not used in the old schema, but + // we don't want to register them as new Scalars + if (ScalarInfo.isGraphqlSpecifiedScalar(scalarName)) { + return; + } + + ScalarAddition addition = new ScalarAddition(scalarName); + scalarDifferences.put(scalarName, addition); + } + + private void addedDirective(EditOperation editOperation) { + String directiveName = editOperation.getTargetVertex().getName(); + + DirectiveAddition addition = new DirectiveAddition(directiveName); + directiveDifferences.put(directiveName, addition); + } + + + private void removedObject(EditOperation editOperation) { + String objectName = editOperation.getSourceVertex().getName(); + + ObjectDeletion change = new ObjectDeletion(objectName); + objectDifferences.put(objectName, change); + } + + private void removedInterface(EditOperation editOperation) { + String interfaceName = editOperation.getSourceVertex().getName(); + + InterfaceDeletion change = new InterfaceDeletion(interfaceName); + interfaceDifferences.put(interfaceName, change); + } + + private void removedUnion(EditOperation editOperation) { + String unionName = editOperation.getSourceVertex().getName(); + + UnionDeletion change = new UnionDeletion(unionName); + unionDifferences.put(unionName, change); + } + + private void removedInputObject(EditOperation editOperation) { + String name = editOperation.getSourceVertex().getName(); + + InputObjectDeletion change = new InputObjectDeletion(name); + inputObjectDifferences.put(name, change); + } + + private void removedEnum(EditOperation editOperation) { + String enumName = editOperation.getSourceVertex().getName(); + + EnumDeletion deletion = new EnumDeletion(enumName); + enumDifferences.put(enumName, deletion); + } + + private void deletedScalar(EditOperation editOperation) { + String scalarName = editOperation.getSourceVertex().getName(); + + ScalarDeletion change = new ScalarDeletion(scalarName); + scalarDifferences.put(scalarName, change); + } + + private void deletedDirective(EditOperation editOperation) { + String directiveName = editOperation.getSourceVertex().getName(); + + DirectiveDeletion change = new DirectiveDeletion(directiveName); + directiveDifferences.put(directiveName, change); + } + + private void argumentDeleted(EditOperation editOperation) { + Vertex deletedArgument = editOperation.getSourceVertex(); + Vertex fieldOrDirective = oldSchemaGraph.getFieldOrDirectiveForArgument(deletedArgument); + if (fieldOrDirective.isOfType(SchemaGraph.FIELD)) { + Vertex field = fieldOrDirective; + Vertex fieldsContainerForField = oldSchemaGraph.getFieldsContainerForField(field); + if (fieldsContainerForField.isOfType(SchemaGraph.OBJECT)) { + Vertex object = fieldsContainerForField; + getObjectModification(object.getName()).getDetails().add(new ObjectFieldArgumentDeletion(field.getName(), deletedArgument.getName())); + } else { + assertTrue(fieldsContainerForField.isOfType(SchemaGraph.INTERFACE)); + Vertex interfaze = fieldsContainerForField; + getInterfaceModification(interfaze.getName()).getDetails().add(new InterfaceFieldArgumentDeletion(field.getName(), deletedArgument.getName())); + } + } else { + assertTrue(fieldOrDirective.isOfType(SchemaGraph.DIRECTIVE)); + Vertex directive = fieldOrDirective; + getDirectiveModification(directive.getName()).getDetails().add(new DirectiveArgumentDeletion(deletedArgument.getName())); + } + + } + + private void argumentAdded(EditOperation editOperation) { + Vertex addedArgument = editOperation.getTargetVertex(); + Vertex fieldOrDirective = newSchemaGraph.getFieldOrDirectiveForArgument(addedArgument); + if (fieldOrDirective.isOfType(SchemaGraph.FIELD)) { + Vertex field = fieldOrDirective; + Vertex fieldsContainerForField = newSchemaGraph.getFieldsContainerForField(field); + if (fieldsContainerForField.isOfType(SchemaGraph.OBJECT)) { + Vertex object = fieldsContainerForField; + getObjectModification(object.getName()).getDetails().add(new ObjectFieldArgumentAddition(field.getName(), addedArgument.getName())); + } else { + assertTrue(fieldsContainerForField.isOfType(SchemaGraph.INTERFACE)); + Vertex interfaze = fieldsContainerForField; + getInterfaceModification(interfaze.getName()).getDetails().add(new InterfaceFieldArgumentAddition(field.getName(), addedArgument.getName())); + } + } else { + assertTrue(fieldOrDirective.isOfType(SchemaGraph.DIRECTIVE)); + Vertex directive = fieldOrDirective; + getDirectiveModification(directive.getName()).getDetails().add(new DirectiveArgumentAddition(addedArgument.getName())); + + } + + } + + private void changedEnum(EditOperation editOperation) { + String oldName = editOperation.getSourceVertex().getName(); + String newName = editOperation.getTargetVertex().getName(); + + EnumModification modification = new EnumModification(oldName, newName); + enumDifferences.put(oldName, modification); + enumDifferences.put(newName, modification); + } + + private void changedScalar(EditOperation editOperation) { + String oldName = editOperation.getSourceVertex().getName(); + String newName = editOperation.getTargetVertex().getName(); + + ScalarModification modification = new ScalarModification(oldName, newName); + scalarDifferences.put(oldName, modification); + scalarDifferences.put(newName, modification); + } + + private void changedInputObject(EditOperation editOperation) { + String oldName = editOperation.getSourceVertex().getName(); + String newName = editOperation.getTargetVertex().getName(); + + InputObjectModification modification = new InputObjectModification(oldName, newName); + inputObjectDifferences.put(oldName, modification); + inputObjectDifferences.put(newName, modification); + } + + private void changedDirective(EditOperation editOperation) { + String oldName = editOperation.getSourceVertex().getName(); + String newName = editOperation.getTargetVertex().getName(); + + DirectiveModification modification = new DirectiveModification(oldName, newName); + directiveDifferences.put(oldName, modification); + directiveDifferences.put(newName, modification); + } + + private void changedObject(EditOperation editOperation) { + String oldName = editOperation.getSourceVertex().getName(); + String newName = editOperation.getTargetVertex().getName(); + + ObjectModification objectModification = new ObjectModification(oldName, newName); + objectDifferences.put(oldName, objectModification); + objectDifferences.put(newName, objectModification); + } + + private void changedInterface(EditOperation editOperation) { + String oldName = editOperation.getSourceVertex().getName(); + String newName = editOperation.getTargetVertex().getName(); + + InterfaceModification interfaceModification = new InterfaceModification(oldName, newName); + interfaceDifferences.put(oldName, interfaceModification); + interfaceDifferences.put(newName, interfaceModification); + } + + private void changedUnion(EditOperation editOperation) { + String newUnionName = editOperation.getTargetVertex().getName(); + String oldUnionName = editOperation.getSourceVertex().getName(); + + UnionModification objectModification = new UnionModification(oldUnionName, newUnionName); + unionDifferences.put(oldUnionName, objectModification); + unionDifferences.put(newUnionName, objectModification); + } +// +// private void changedUnion(EditOperation editOperation) { +// // object changes include: adding/removing Interface, adding/removing applied directives, changing name +// String objectName = editOperation.getTargetVertex().getName(); +// +// ObjectAdded objectAdded = new ObjectAdded(objectName); +// changes.add(objectAdded); +// } +// +// private void changedEnum(EditOperation editOperation) { +// // object changes include: adding/removing Interface, adding/removing applied directives, changing name +// String objectName = editOperation.getTargetVertex().getName(); +// +// ObjectAdded objectAdded = new ObjectAdded(objectName); +// changes.add(objectAdded); +// } +// +// private void changedInputObject(EditOperation editOperation) { +// // object changes include: adding/removing Interface, adding/removing applied directives, changing name +// String objectName = editOperation.getTargetVertex().getName(); +// +// ObjectAdded objectAdded = new ObjectAdded(objectName); +// changes.add(objectAdded); +// } +// +// private void changedScalar(EditOperation editOperation) { +// // object changes include: adding/removing Interface, adding/removing applied directives, changing name +// String objectName = editOperation.getTargetVertex().getName(); +// +// ObjectAdded objectAdded = new ObjectAdded(objectName); +// changes.add(objectAdded); +// } +// +// private void changedField(EditOperation editOperation) { +// // object changes include: adding/removing Interface, adding/removing applied directives, changing name +// Vertex field = editOperation.getTargetVertex(); +// Vertex fieldsContainerForField = newSchemaGraph.getFieldsContainerForField(field); +// +// FieldModification objectAdded = new FieldModification(field.getName(), fieldsContainerForField.getName()); +// changes.add(objectAdded); +// } +// +// private void changedInputField(EditOperation editOperation) { +// // object changes include: adding/removing Interface, adding/removing applied directives, changing name +// String objectName = editOperation.getTargetVertex().getName(); +// +// ObjectAdded objectAdded = new ObjectAdded(objectName); +// changes.add(objectAdded); +// } + + +} diff --git a/src/main/java/graphql/schema/diffing/ana/SchemaDifference.java b/src/main/java/graphql/schema/diffing/ana/SchemaDifference.java new file mode 100644 index 0000000000..6c4b1bcbf9 --- /dev/null +++ b/src/main/java/graphql/schema/diffing/ana/SchemaDifference.java @@ -0,0 +1,1566 @@ +package graphql.schema.diffing.ana; + +import graphql.Internal; +import graphql.util.FpKit; + +import java.util.ArrayList; +import java.util.List; + +/** + * Any kind of difference between two schemas is a SchemaDifference. + * + * Below that we have three different possible kind of differences: + * - Addition + * - Deletion + * - Modification + */ +@Internal +public interface SchemaDifference { + + interface SchemaAddition extends SchemaDifference { + + } + + interface SchemaDeletion extends SchemaDifference { + + } + + interface SchemaModification extends SchemaDifference { + + } + + interface SchemaModificationDetail extends SchemaDifference { + + } + + //------------ Object + public + interface ObjectDifference extends SchemaDifference { + + } + + class ObjectAddition implements SchemaAddition, ObjectDifference { + private final String name; + + public ObjectAddition(String name) { + this.name = name; + } + + public String getName() { + return name; + } + } + + class ObjectDeletion implements SchemaDeletion, ObjectDifference { + private final String name; + + public ObjectDeletion(String name) { + this.name = name; + } + + public String getName() { + return name; + } + } + + class ObjectModification implements SchemaModification, ObjectDifference { + private final String oldName; + private final String newName; + private final boolean renamed; + private final List details = new ArrayList<>(); + + public ObjectModification(String oldName, String newName) { + this.oldName = oldName; + this.newName = newName; + this.renamed = oldName.equals(newName); + } + + public ObjectModification(String newName) { + this.oldName = newName; + this.newName = newName; + this.renamed = false; + } + + public List getDetails() { + return details; + } + + public List getDetails(Class clazz) { + return (List) FpKit.filterList(details, clazz::isInstance); + } + + public String getOldName() { + return oldName; + } + + public String getNewName() { + return newName; + } + + public boolean isRenamed() { + return renamed; + } + } + + interface ObjectModificationDetail { + + } + + class ObjectInterfaceImplementationAddition implements ObjectModificationDetail { + private final String name; + + public ObjectInterfaceImplementationAddition(String name) { + this.name = name; + } + + public String getName() { + return name; + } + } + + class ObjectInterfaceImplementationDeletion implements ObjectModificationDetail { + private final String name; + + public ObjectInterfaceImplementationDeletion(String name) { + this.name = name; + } + + public String getName() { + return name; + } + } + + class ObjectFieldAddition implements ObjectModificationDetail { + private final String name; + + public ObjectFieldAddition(String name) { + this.name = name; + } + + public String getName() { + return name; + } + } + + class ObjectFieldDeletion implements ObjectModificationDetail { + private final String name; + + public ObjectFieldDeletion(String name) { + this.name = name; + } + + public String getName() { + return name; + } + } + + class ObjectFieldRename implements ObjectModificationDetail { + private final String oldName; + private final String newName; + + public ObjectFieldRename(String oldName, String newName) { + this.oldName = oldName; + this.newName = newName; + } + + public String getNewName() { + return newName; + } + + public String getOldName() { + return oldName; + } + } + + class ObjectFieldArgumentRename implements ObjectModificationDetail { + private final String oldName; + private final String newName; + + public ObjectFieldArgumentRename(String oldName, String newName) { + this.oldName = oldName; + this.newName = newName; + } + + public String getNewName() { + return newName; + } + + public String getOldName() { + return oldName; + } + } + + class ObjectFieldTypeModification implements ObjectModificationDetail { + private final String fieldName; + private final String oldType; + private final String newType; + + public ObjectFieldTypeModification(String fieldName, String oldType, String newType) { + this.fieldName = fieldName; + this.oldType = oldType; + this.newType = newType; + } + + public String getFieldName() { + return fieldName; + } + + public String getNewType() { + return newType; + } + + public String getOldType() { + return oldType; + } + } + + class ObjectFieldArgumentDeletion implements ObjectModificationDetail { + private final String fieldName; + private final String name; + + public ObjectFieldArgumentDeletion(String fieldName, String name) { + this.fieldName = fieldName; + this.name = name; + } + + public String getName() { + return name; + } + + public String getFieldName() { + return fieldName; + } + } + + class ObjectFieldArgumentAddition implements ObjectModificationDetail { + private final String fieldName; + private final String name; + + + public ObjectFieldArgumentAddition(String fieldName, String name) { + this.fieldName = fieldName; + this.name = name; + } + + public String getFieldName() { + return fieldName; + } + + public String getName() { + return name; + } + } + + class ObjectFieldArgumentTypeModification implements ObjectModificationDetail { + private final String fieldName; + private final String argumentName; + private final String oldType; + private final String newType; + + public ObjectFieldArgumentTypeModification(String fieldName, String argumentName, String oldType, String newType) { + this.fieldName = fieldName; + this.argumentName = argumentName; + this.oldType = oldType; + this.newType = newType; + } + + public String getNewType() { + return newType; + } + + public String getOldType() { + return oldType; + } + + public String getFieldName() { + return fieldName; + } + + public String getArgumentName() { + return argumentName; + } + } + + class ObjectFieldArgumentDefaultValueModification implements ObjectModificationDetail { + private final String fieldName; + private final String argumentName; + private final String oldValue; + private final String newValue; + + public ObjectFieldArgumentDefaultValueModification(String fieldName, String argumentName, String oldValue, String newValue) { + this.fieldName = fieldName; + this.argumentName = argumentName; + this.oldValue = oldValue; + this.newValue = newValue; + } + + public String getOldValue() { + return oldValue; + } + + public String getNewValue() { + return newValue; + } + + public String getFieldName() { + return fieldName; + } + + public String getArgumentName() { + return argumentName; + } + } + + //------------ Interface + interface InterfaceDifference extends SchemaDifference { + + } + + class InterfaceAddition implements SchemaAddition, InterfaceDifference { + private final String name; + + public InterfaceAddition(String name) { + this.name = name; + } + + public String getName() { + return name; + } + } + + class InterfaceDeletion implements SchemaDeletion, InterfaceDifference { + private final String name; + + public InterfaceDeletion(String name) { + this.name = name; + } + } + + class InterfaceModification implements SchemaModification, InterfaceDifference { + private final String oldName; + private final String newName; + private final boolean renamed; + private final List details = new ArrayList<>(); + + public InterfaceModification(String oldName, String newName) { + this.oldName = oldName; + this.newName = newName; + this.renamed = oldName.equals(newName); + } + + public InterfaceModification(String newName) { + this.oldName = newName; + this.newName = newName; + this.renamed = false; + } + + public List getDetails() { + return details; + } + + public String getNewName() { + return newName; + } + + public String getOldName() { + return oldName; + } + + public boolean isRenamed() { + return renamed; + } + + public List getDetails(Class clazz) { + return (List) FpKit.filterList(details, clazz::isInstance); + } + } + + interface InterfaceModificationDetail { + + } + + class InterfaceInterfaceImplementationAddition implements InterfaceModificationDetail { + private final String name; + + public InterfaceInterfaceImplementationAddition(String name) { + this.name = name; + } + + public String getName() { + return name; + } + } + + class InterfaceInterfaceImplementationDeletion implements InterfaceModificationDetail { + private final String name; + + public InterfaceInterfaceImplementationDeletion(String name) { + this.name = name; + } + + public String getName() { + return name; + } + } + + class InterfaceFieldAddition implements InterfaceModificationDetail { + private final String name; + + public InterfaceFieldAddition(String name) { + this.name = name; + } + + public String getName() { + return name; + } + } + + class InterfaceFieldDeletion implements InterfaceModificationDetail { + private final String name; + + public InterfaceFieldDeletion(String name) { + this.name = name; + } + + public String getName() { + return name; + } + } + + class InterfaceFieldRename implements InterfaceModificationDetail { + private final String oldName; + private final String newName; + + public InterfaceFieldRename(String oldName, String newName) { + this.oldName = oldName; + this.newName = newName; + } + + public String getNewName() { + return newName; + } + + public String getOldName() { + return oldName; + } + } + + + class InterfaceFieldTypeModification implements InterfaceModificationDetail { + private final String fieldName; + private final String oldType; + private final String newType; + + public InterfaceFieldTypeModification(String fieldName, String oldType, String newType) { + this.fieldName = fieldName; + this.oldType = oldType; + this.newType = newType; + } + + public String getFieldName() { + return fieldName; + } + + public String getNewType() { + return newType; + } + + public String getOldType() { + return oldType; + } + } + + class InterfaceFieldArgumentDeletion implements InterfaceModificationDetail { + private final String fieldName; + private final String name; + + + public InterfaceFieldArgumentDeletion(String fieldName, String name) { + this.fieldName = fieldName; + this.name = name; + } + + public String getFieldName() { + return fieldName; + } + + public String getName() { + return name; + } + } + + class InterfaceFieldArgumentAddition implements InterfaceModificationDetail { + private final String fieldName; + private final String name; + + public InterfaceFieldArgumentAddition(String fieldName, String name) { + this.fieldName = fieldName; + this.name = name; + } + + public String getFieldName() { + return fieldName; + } + + public String getName() { + return name; + } + } + + class InterfaceFieldArgumentTypeModification implements InterfaceModificationDetail { + + private String fieldName; + private final String argumentName; + private final String oldType; + private final String newType; + + public InterfaceFieldArgumentTypeModification(String fieldName, String argumentName, String oldType, String newType) { + this.fieldName = fieldName; + this.argumentName = argumentName; + this.oldType = oldType; + this.newType = newType; + } + + public String getFieldName() { + return fieldName; + } + + public String getNewType() { + return newType; + } + + public String getOldType() { + return oldType; + } + + public String getArgumentName() { + return argumentName; + } + + } + + class InterfaceFieldArgumentDefaultValueModification implements InterfaceModificationDetail { + private final String fieldName; + private final String argumentName; + private final String oldValue; + private final String newValue; + + + public InterfaceFieldArgumentDefaultValueModification(String fieldName, String argumentName, String oldValue, String newValue) { + this.fieldName = fieldName; + this.argumentName = argumentName; + this.oldValue = oldValue; + this.newValue = newValue; + } + + public String getOldValue() { + return oldValue; + } + + public String getNewValue() { + return newValue; + } + + public String getFieldName() { + return fieldName; + } + + public String getArgumentName() { + return argumentName; + } + } + + class InterfaceFieldArgumentRename implements InterfaceModificationDetail { + private final String oldName; + private final String newName; + + public InterfaceFieldArgumentRename(String oldName, String newName) { + this.oldName = oldName; + this.newName = newName; + } + + public String getNewName() { + return newName; + } + + public String getOldName() { + return oldName; + } + } + + + // -----Union----------- + interface UnionDifference extends SchemaDifference { + + } + + class UnionAddition implements SchemaAddition, UnionDifference { + private final String name; + + public UnionAddition(String name) { + this.name = name; + } + + public String getName() { + return name; + } + } + + class UnionDeletion implements SchemaDeletion, UnionDifference { + private final String name; + + public UnionDeletion(String name) { + this.name = name; + } + + public String getName() { + return name; + } + } + + class UnionModification implements SchemaModification, UnionDifference { + private final String oldName; + private final String newName; + private final boolean nameChanged; + + private final List details = new ArrayList<>(); + + public UnionModification(String oldName, String newName) { + this.oldName = oldName; + this.newName = newName; + this.nameChanged = oldName.equals(newName); + } + + public UnionModification(String newName) { + this.oldName = newName; + this.newName = newName; + this.nameChanged = false; + } + + public String getNewName() { + return newName; + } + + public String getOldName() { + return oldName; + } + + public List getDetails() { + return details; + } + + public List getDetails(Class clazz) { + return (List) FpKit.filterList(details, clazz::isInstance); + } + + public boolean isNameChanged() { + return nameChanged; + } + } + + interface UnionModificationDetail { + + } + + class UnionMemberAddition implements UnionModificationDetail { + private final String name; + + public UnionMemberAddition(String name) { + this.name = name; + } + + public String getName() { + return name; + } + } + + class UnionMemberDeletion implements UnionModificationDetail { + private final String name; + + public UnionMemberDeletion(String name) { + this.name = name; + } + + public String getName() { + return name; + } + } + + //--------InputObject + + interface InputObjectDifference extends SchemaDifference { + + } + + class InputObjectAddition implements SchemaAddition, InputObjectDifference { + private final String name; + + public InputObjectAddition(String name) { + this.name = name; + } + + public String getName() { + return name; + } + } + + class InputObjectDeletion implements SchemaDeletion, InputObjectDifference { + private final String name; + + public InputObjectDeletion(String name) { + this.name = name; + } + + public String getName() { + return name; + } + } + + interface InputObjectModificationDetail { + + } + + class InputObjectFieldDeletion implements InputObjectModificationDetail { + private final String name; + + public InputObjectFieldDeletion(String name) { + this.name = name; + } + + public String getName() { + return name; + } + } + + class InputObjectFieldRename implements InputObjectModificationDetail { + private final String oldName; + private final String newName; + + public InputObjectFieldRename(String oldName, String newName) { + this.oldName = oldName; + this.newName = newName; + } + + public String getOldName() { + return oldName; + } + + public String getNewName() { + return newName; + } + } + + class InputObjectFieldDefaultValueModification implements InputObjectModificationDetail { + private final String fieldName; + private final String oldDefaultValue; + private final String newDefaultValue; + + public InputObjectFieldDefaultValueModification(String fieldName, String oldDefaultValue, String newDefaultValue) { + this.fieldName = fieldName; + this.oldDefaultValue = oldDefaultValue; + this.newDefaultValue = newDefaultValue; + } + + public String getFieldName() { + return fieldName; + } + + public String getOldDefaultValue() { + return oldDefaultValue; + } + + public String getNewDefaultValue() { + return newDefaultValue; + } + } + + class InputObjectFieldTypeModification implements InputObjectModificationDetail { + private final String fieldName; + private final String oldType; + private final String newType; + + public InputObjectFieldTypeModification(String fieldName, String oldType, String newType) { + this.fieldName = fieldName; + this.oldType = oldType; + this.newType = newType; + } + + public String getFieldName() { + return fieldName; + } + + public String getOldType() { + return oldType; + } + + public String getNewType() { + return newType; + } + } + + class InputObjectFieldAddition implements InputObjectModificationDetail { + private final String name; + + public InputObjectFieldAddition(String name) { + this.name = name; + } + + public String getName() { + return name; + } + } + + class InputObjectModification implements SchemaModification, InputObjectDifference { + private final String oldName; + private final String newName; + private final boolean nameChanged; + + private final List details = new ArrayList<>(); + + + public InputObjectModification(String oldName, String newName) { + this.oldName = oldName; + this.newName = newName; + this.nameChanged = oldName.equals(newName); + } + + public InputObjectModification(String newName) { + this.oldName = newName; + this.newName = newName; + this.nameChanged = false; + } + + public boolean isNameChanged() { + return nameChanged; + } + + public String getNewName() { + return newName; + } + + public String getOldName() { + return oldName; + } + + public List getDetails() { + return details; + } + + public List getDetails(Class clazz) { + return (List) FpKit.filterList(details, clazz::isInstance); + } + + } + + //-------Enum + interface EnumDifference extends SchemaDifference { + + } + + class EnumAddition implements SchemaAddition, EnumDifference { + private final String name; + + public EnumAddition(String name) { + this.name = name; + } + + public String getName() { + return name; + } + } + + class EnumDeletion implements SchemaDeletion, EnumDifference { + private final String name; + + public EnumDeletion(String name) { + this.name = name; + } + + public String getName() { + return name; + } + } + + class EnumModification implements SchemaModification, EnumDifference { + private final String oldName; + private final String newName; + + private final boolean nameChanged; + private final List details = new ArrayList<>(); + + public EnumModification(String oldName, String newName) { + this.oldName = oldName; + this.newName = newName; + this.nameChanged = oldName.equals(newName); + } + + public EnumModification(String newName) { + this.oldName = newName; + this.newName = newName; + this.nameChanged = false; + } + + public boolean isNameChanged() { + return nameChanged; + } + + public String getNewName() { + return newName; + } + + public String getOldName() { + return oldName; + } + + public List getDetails() { + return details; + } + + public List getDetails(Class clazz) { + return (List) FpKit.filterList(details, clazz::isInstance); + } + + } + + interface EnumModificationDetail { + + } + + class EnumValueDeletion implements EnumModificationDetail { + private final String name; + + public EnumValueDeletion(String name) { + this.name = name; + } + + public String getName() { + return name; + } + } + + class EnumValueAddition implements EnumModificationDetail { + private final String name; + + public EnumValueAddition(String name) { + this.name = name; + } + + public String getName() { + return name; + } + } + + //--------Scalar + interface ScalarDifference extends SchemaDifference { + + } + + class ScalarAddition implements SchemaAddition, ScalarDifference { + private final String name; + + public ScalarAddition(String name) { + this.name = name; + } + + public String getName() { + return name; + } + } + + class ScalarDeletion implements SchemaDeletion, ScalarDifference { + private final String name; + + public ScalarDeletion(String name) { + this.name = name; + } + + public String getName() { + return name; + } + } + + interface ScalarModificationDetail { + + } + + class ScalarModification implements SchemaModification, ScalarDifference { + private final String oldName; + private final String newName; + private final boolean nameChanged; + private List details = new ArrayList<>(); + + + public ScalarModification(String oldName, String newName) { + this.oldName = oldName; + this.newName = newName; + this.nameChanged = oldName.equals(newName); + } + + public ScalarModification(String newName) { + this.oldName = newName; + this.newName = newName; + this.nameChanged = false; + } + + + public boolean isNameChanged() { + return nameChanged; + } + + public String getNewName() { + return newName; + } + + public String getOldName() { + return oldName; + } + + public List getDetails() { + return details; + } + + public List getDetails(Class clazz) { + return (List) FpKit.filterList(details, clazz::isInstance); + } + + } + + //------Directive + interface DirectiveDifference extends SchemaDifference { + + } + + class DirectiveAddition implements SchemaAddition, DirectiveDifference { + private final String name; + + public DirectiveAddition(String name) { + this.name = name; + } + + public String getName() { + return name; + } + } + + class DirectiveDeletion implements SchemaDeletion, DirectiveDifference { + private final String name; + + public DirectiveDeletion(String name) { + this.name = name; + } + + public String getName() { + return name; + } + } + + class DirectiveModification implements SchemaModification, DirectiveDifference { + private final String oldName; + private final String newName; + private final boolean nameChanged; + + private final List details = new ArrayList<>(); + + public DirectiveModification(String oldName, String newName) { + this.oldName = oldName; + this.newName = newName; + this.nameChanged = oldName.equals(newName); + } + + public DirectiveModification(String newName) { + this.oldName = newName; + this.newName = newName; + this.nameChanged = false; + } + + public boolean isNameChanged() { + return nameChanged; + } + + public String getNewName() { + return newName; + } + + public String getOldName() { + return oldName; + } + + public List getDetails() { + return details; + } + + public List getDetails(Class clazz) { + return (List) FpKit.filterList(details, clazz::isInstance); + } + } + + interface DirectiveModificationDetail { + + } + + class DirectiveArgumentDeletion implements DirectiveModificationDetail { + private final String name; + + public DirectiveArgumentDeletion(String name) { + this.name = name; + } + + public String getName() { + return name; + } + + } + + class DirectiveArgumentAddition implements DirectiveModificationDetail { + private final String name; + + public DirectiveArgumentAddition(String name) { + this.name = name; + } + + public String getName() { + return name; + } + } + + class DirectiveArgumentTypeModification implements DirectiveModificationDetail { + private final String argumentName; + private final String oldType; + private final String newType; + + + public DirectiveArgumentTypeModification(String argumentName, String oldType, String newType) { + this.argumentName = argumentName; + this.oldType = oldType; + this.newType = newType; + } + + public String getArgumentName() { + return argumentName; + } + + public String getNewType() { + return newType; + } + + public String getOldType() { + return oldType; + } + } + + class DirectiveArgumentDefaultValueModification implements DirectiveModificationDetail { + private final String argumentName; + private final String oldValue; + private final String newValue; + + public DirectiveArgumentDefaultValueModification(String argumentName, String oldValue, String newValue) { + this.argumentName = argumentName; + this.oldValue = oldValue; + this.newValue = newValue; + } + + public String getOldValue() { + return oldValue; + } + + public String getNewValue() { + return newValue; + } + + public String getArgumentName() { + return argumentName; + } + } + + class DirectiveArgumentRename implements DirectiveModificationDetail { + private final String oldName; + private final String newName; + + public DirectiveArgumentRename(String oldName, String newName) { + this.oldName = oldName; + this.newName = newName; + } + + public String getNewName() { + return newName; + } + + public String getOldName() { + return oldName; + } + } + + //------Applied Directives + interface AppliedDirectiveDifference { + + } + + /** + * SCHEMA, + * SCALAR, + * OBJECT, + * FIELD_DEFINITION, + * ARGUMENT_DEFINITION, + * INTERFACE, + * UNION, + * ENUM, + * ENUM_VALUE, + * INPUT_OBJECT, + * INPUT_FIELD_DEFINITION + */ + + interface AppliedDirectiveLocationDetail { + + } + + class AppliedDirectiveObjectFieldLocation implements AppliedDirectiveLocationDetail { + private final String objectName; + private final String fieldName; + + public AppliedDirectiveObjectFieldLocation(String objectName, String fieldName) { + this.objectName = objectName; + this.fieldName = fieldName; + } + + public String getFieldName() { + return fieldName; + } + + public String getObjectName() { + return objectName; + } + } + + class AppliedDirectiveInterfaceFieldLocation implements AppliedDirectiveLocationDetail { + private final String interfaceName; + private final String fieldName; + + public AppliedDirectiveInterfaceFieldLocation(String interfaceName, String fieldName) { + this.interfaceName = interfaceName; + this.fieldName = fieldName; + } + + public String getFieldName() { + return fieldName; + } + + public String getInterfaceName() { + return interfaceName; + } + } + + class AppliedDirectiveScalarLocation implements AppliedDirectiveLocationDetail { + private final String name; + + public AppliedDirectiveScalarLocation(String name) { + this.name = name; + } + + public String getName() { + return name; + } + } + + class AppliedDirectiveSchemaLocation implements AppliedDirectiveLocationDetail { + + } + + class AppliedDirectiveObjectLocation implements AppliedDirectiveLocationDetail { + private final String name; + + public AppliedDirectiveObjectLocation(String name) { + this.name = name; + } + + public String getName() { + return name; + } + } + + class AppliedDirectiveInterfaceLocation implements AppliedDirectiveLocationDetail { + private final String name; + + public AppliedDirectiveInterfaceLocation(String name) { + this.name = name; + } + + public String getName() { + return name; + } + } + + class AppliedDirectiveObjectFieldArgumentLocation implements AppliedDirectiveLocationDetail { + private final String objectName; + private final String fieldName; + private final String argumentName; + + public AppliedDirectiveObjectFieldArgumentLocation(String objectName, String fieldName, String argumentName) { + this.objectName = objectName; + this.fieldName = fieldName; + this.argumentName = argumentName; + } + + public String getObjectName() { + return objectName; + } + + public String getFieldName() { + return fieldName; + } + + public String getArgumentName() { + return argumentName; + } + } + + class AppliedDirectiveDirectiveArgumentLocation implements AppliedDirectiveLocationDetail { + private final String directiveName; + private final String argumentName; + + public AppliedDirectiveDirectiveArgumentLocation(String directiveName, String argumentName) { + this.directiveName = directiveName; + this.argumentName = argumentName; + } + + public String getDirectiveName() { + return directiveName; + } + + public String getArgumentName() { + return argumentName; + } + } + + class AppliedDirectiveInterfaceFieldArgumentLocation implements AppliedDirectiveLocationDetail { + private final String interfaceName; + private final String fieldName; + private final String argumentName; + + public AppliedDirectiveInterfaceFieldArgumentLocation(String interfaceName, String fieldName, String argumentName) { + this.interfaceName = interfaceName; + this.fieldName = fieldName; + this.argumentName = argumentName; + } + + public String getInterfaceName() { + return interfaceName; + } + + public String getFieldName() { + return fieldName; + } + + public String getArgumentName() { + return argumentName; + } + } + + class AppliedDirectiveUnionLocation implements AppliedDirectiveLocationDetail { + private final String name; + + public AppliedDirectiveUnionLocation(String name) { + this.name = name; + } + + public String getName() { + return name; + } + } + + class AppliedDirectiveEnumLocation implements AppliedDirectiveLocationDetail { + private final String name; + + public AppliedDirectiveEnumLocation(String name) { + this.name = name; + } + + public String getName() { + return name; + } + } + + class AppliedDirectiveEnumValueLocation implements AppliedDirectiveLocationDetail { + private final String enumName; + private final String valueName; + + public AppliedDirectiveEnumValueLocation(String enumName, String valueName) { + this.enumName = enumName; + this.valueName = valueName; + } + + public String getEnumName() { + return enumName; + } + + public String getValueName() { + return valueName; + } + } + + class AppliedDirectiveInputObjectLocation implements AppliedDirectiveLocationDetail { + private final String name; + + public AppliedDirectiveInputObjectLocation(String name) { + this.name = name; + } + + public String getName() { + return name; + } + } + + + class AppliedDirectiveInputObjectFieldLocation implements AppliedDirectiveLocationDetail { + private final String inputObjectName; + private final String fieldName; + + public AppliedDirectiveInputObjectFieldLocation(String inputObjectName, String fieldName) { + this.inputObjectName = inputObjectName; + this.fieldName = fieldName; + } + + public String getInputObjectName() { + return inputObjectName; + } + + public String getFieldName() { + return fieldName; + } + } + + class AppliedDirectiveAddition implements + ObjectModificationDetail, + InterfaceModificationDetail, + ScalarModificationDetail, + EnumModificationDetail, + InputObjectModificationDetail, + UnionModificationDetail, + DirectiveModificationDetail { + private final AppliedDirectiveLocationDetail locationDetail; + private final String name; + + public AppliedDirectiveAddition(AppliedDirectiveLocationDetail locationDetail, String name) { + this.locationDetail = locationDetail; + this.name = name; + } + + public String getName() { + return name; + } + + public AppliedDirectiveLocationDetail getLocationDetail() { + return locationDetail; + } + } + + class AppliedDirectiveDeletion implements + ObjectModificationDetail, + InterfaceModificationDetail, + ScalarModificationDetail, + EnumModificationDetail, + InputObjectModificationDetail, + UnionModificationDetail, + DirectiveModificationDetail { + + private final AppliedDirectiveLocationDetail locationDetail; + private final String name; + + public AppliedDirectiveDeletion(AppliedDirectiveLocationDetail locationDetail, String name) { + this.locationDetail = locationDetail; + this.name = name; + } + + public String getName() { + return name; + } + + public AppliedDirectiveLocationDetail getLocationDetail() { + return locationDetail; + } + + + } + + class AppliedDirectiveRenamed { + + } + + class AppliedDirectiveArgumentAddition { + + } + + class AppliedDirectiveArgumentDeletion implements ObjectModificationDetail, InterfaceModificationDetail { + private final AppliedDirectiveLocationDetail locationDetail; + private final String argumentName; + + public AppliedDirectiveArgumentDeletion(AppliedDirectiveLocationDetail locationDetail, String argumentName) { + this.locationDetail = locationDetail; + this.argumentName = argumentName; + } + + public AppliedDirectiveLocationDetail getLocationDetail() { + return locationDetail; + } + + public String getArgumentName() { + return argumentName; + } + } + + + class AppliedDirectiveArgumentValueModification implements ObjectModificationDetail { + private final AppliedDirectiveLocationDetail locationDetail; + private final String argumentName; + private final String oldValue; + private final String newValue; + + public AppliedDirectiveArgumentValueModification(AppliedDirectiveLocationDetail locationDetail, String argumentName, String oldValue, String newValue) { + this.locationDetail = locationDetail; + this.argumentName = argumentName; + this.oldValue = oldValue; + this.newValue = newValue; + } + + public AppliedDirectiveLocationDetail getLocationDetail() { + return locationDetail; + } + + public String getArgumentName() { + return argumentName; + } + + public String getOldValue() { + return oldValue; + } + + public String getNewValue() { + return newValue; + } + } + + class AppliedDirectiveArgumentRename implements ObjectModificationDetail { + private final AppliedDirectiveLocationDetail locationDetail; + private final String oldName; + private final String newName; + + public AppliedDirectiveArgumentRename(AppliedDirectiveLocationDetail locationDetail, String oldName, String newName) { + this.locationDetail = locationDetail; + this.oldName = oldName; + this.newName = newName; + } + + public AppliedDirectiveLocationDetail getLocationDetail() { + return locationDetail; + } + + public String getOldName() { + return oldName; + } + + public String getNewName() { + return newName; + } + } + + +} diff --git a/src/main/java/graphql/schema/diffing/dot/Dotfile.java b/src/main/java/graphql/schema/diffing/dot/Dotfile.java new file mode 100644 index 0000000000..6e7e51dfc4 --- /dev/null +++ b/src/main/java/graphql/schema/diffing/dot/Dotfile.java @@ -0,0 +1,124 @@ +package graphql.schema.diffing.dot; + +import java.util.ArrayList; +import java.util.List; + + +public class Dotfile { + + public static class Node { + public Node(String id, String label, String color) { + this.id = id; + this.label = label; + this.color = color; + } + + String id; + String label; + String color; + } + + public static class Edge { + public Edge(String from, String to, String label) { + this.from = from; + this.to = to; + this.label = label; + } + + String from; + String to; + String label; + } + + public static class SubGraph { + + String id; + String label; + List edges = new ArrayList<>(); + List nodes = new ArrayList<>(); + + public SubGraph(String id, String label) { + this.id = id; + this.label = label; + } + + public String getId() { + return id; + } + + public void addEdge(Edge e) { + edges.add(e); + } + + public void addNode(Node node) { + nodes.add(node); + } + + } + + + private List nodes = new ArrayList<>(); + private List edges = new ArrayList<>(); +// private List subGraphs = new ArrayList<>(); + + + public void addNode(Node node) { + nodes.add(node); + } + + public void addNode(String id, String label, String color) { + nodes.add(new Node(id, label, color)); + } + + public void addEdge(String from, String to, String label) { + edges.add(new Edge(from, to, label)); + } + + public void addEdge(Edge e) { + edges.add(e); + } + +// public void addSubgraph(SubGraph subGraph) { +// subGraphs.add(subGraph); +// } + + public String getId() { + return ""; + } + + public String print() { + StringBuilder result = new StringBuilder(); + result.append("graph G {\n"); + for (Node node : nodes) { + result.append(node.id).append("[label=\"").append(node.label).append("\" color=").append(node.color).append(" style=filled").append("];\n"); + } + for (Edge edge : edges) { + result.append(edge.from).append(" -- ").append(edge.to).append("[label=\"").append(edge.label).append("\"];\n"); + } +// for (SubGraph subGraph : subGraphs) { +// result.append("subgraph cluster_").append(subGraph.id).append("{\n").append("label=\"").append(subGraph.label).append("\";\n"); +// for (Node node : subGraph.nodes) { +// result.append(node.id).append("[label=\"").append(node.label).append("\" color=").append(node.color).append(" style=filled").append("];\n"); +// } +// for (Edge edge : subGraph.edges) { +// result.append(edge.from).append(" -- ").append(edge.to).append("[label=\"").append(edge.label).append("\"];\n"); +// } +// result.append("}"); +// +// } +// result.append(explanation()); + result.append("}"); + return result.toString(); + } + + String explanation() { + return "subgraph cluster_explanation {\n" + + "label=\"Explanation\";\n" + + "concept [color=green style=filled label=\"Concept\"];\n" + + "orgEntity [color=lightblue style=filled label=\"Org Entity\"];\n" + + "product [color=red style=filled label=\"Product\"];\n" + + "concept -> product [style=invis];\n" + + "orgEntity -> concept [style=invis];\n" + + "}"; + } +} diff --git a/src/main/java/graphql/schema/fetching/LambdaFetchingSupport.java b/src/main/java/graphql/schema/fetching/LambdaFetchingSupport.java new file mode 100644 index 0000000000..0056819b36 --- /dev/null +++ b/src/main/java/graphql/schema/fetching/LambdaFetchingSupport.java @@ -0,0 +1,207 @@ +package graphql.schema.fetching; + +import graphql.Internal; +import graphql.VisibleForTesting; + +import java.lang.invoke.CallSite; +import java.lang.invoke.LambdaMetafactory; +import java.lang.invoke.MethodHandle; +import java.lang.invoke.MethodHandles; +import java.lang.invoke.MethodType; +import java.lang.reflect.Method; +import java.lang.reflect.Modifier; +import java.util.ArrayList; +import java.util.Comparator; +import java.util.List; +import java.util.Optional; +import java.util.function.Function; +import java.util.function.Predicate; + +import static java.util.stream.Collectors.toList; + +@Internal +public class LambdaFetchingSupport { + + + /** + * This support class will use {@link LambdaMetafactory} and {@link MethodHandles} to create a dynamic function that allows access to a public + * getter method on the nominated class. {@link MethodHandles} is a caller senstive lookup mechanism. If the graphql-java cant lookup a class, then + * it won't be able to make dynamic lambda function to it. + *

+ * If one cant be made, because it doesn't exist or the calling class does not have access to the method, then it will return + * an empty result indicating that this strategy cant be used. + * + * @param sourceClass the class that has the property getter method + * @param propertyName the name of the property to get + * + * @return a function that can be used to pass in an instance of source class and returns its getter method value + */ + public static Optional> createGetter(Class sourceClass, String propertyName) { + Method candidateMethod = getCandidateMethod(sourceClass, propertyName); + if (candidateMethod != null) { + try { + Function getterFunction = mkCallFunction(sourceClass, candidateMethod.getName(), candidateMethod.getReturnType()); + return Optional.of(getterFunction); + } catch (Throwable ignore) { + // + // if we cant make a dynamic lambda here, then we give up and let the old property fetching code do its thing + // this can happen on runtimes such as GraalVM native where LambdaMetafactory is not supported + // and will throw something like : + // + // com.oracle.svm.core.jdk.UnsupportedFeatureError: Defining hidden classes at runtime is not supported. + // at org.graalvm.nativeimage.builder/com.oracle.svm.core.util.VMError.unsupportedFeature(VMError.java:89) + } + } + return Optional.empty(); + } + + + private static Method getCandidateMethod(Class sourceClass, String propertyName) { + // property() methods first + Predicate recordLikePredicate = method -> isRecordLike(method) && propertyName.equals(decapitalize(method.getName())); + List recordLikeMethods = findMethodsForProperty(sourceClass, + recordLikePredicate); + if (!recordLikeMethods.isEmpty()) { + return recordLikeMethods.get(0); + } + + // getProperty() POJO methods next + Predicate getterPredicate = method -> isGetterNamed(method) && propertyName.equals(mkPropertyNameGetter(method)); + List allGetterMethods = findMethodsForProperty(sourceClass, + getterPredicate); + List pojoGetterMethods = allGetterMethods.stream() + .filter(LambdaFetchingSupport::isPossiblePojoMethod) + .collect(toList()); + if (!pojoGetterMethods.isEmpty()) { + Method method = pojoGetterMethods.get(0); + if (isBooleanGetter(method)) { + method = findBestBooleanGetter(pojoGetterMethods); + } + return checkForSingleParameterPeer(method, allGetterMethods); + } + return null; + } + + private static Method checkForSingleParameterPeer(Method candidateMethod, List allMethods) { + // getFoo(DataFetchingEnv ev) is allowed, but we don't want to handle it in this class + // so this find those edge cases + for (Method allMethod : allMethods) { + if (allMethod.getParameterCount() > 0) { + // we have some method with the property name that takes more than 1 argument + // we don't want to handle this here, so we are saying there is one + return null; + } + } + return candidateMethod; + } + + private static Method findBestBooleanGetter(List methods) { + // we prefer isX() over getX() if both happen to be present + Optional isMethod = methods.stream().filter(method -> method.getName().startsWith("is")).findFirst(); + return isMethod.orElse(methods.get(0)); + } + + /** + * Finds all methods in a class hierarchy that match the property name - they might not be suitable but they + * + * @param sourceClass the class we are looking to work on + * + * @return a list of getter methods for that property + */ + private static List findMethodsForProperty(Class sourceClass, Predicate predicate) { + List methods = new ArrayList<>(); + Class currentClass = sourceClass; + while (currentClass != null) { + Method[] declaredMethods = currentClass.getDeclaredMethods(); + for (Method declaredMethod : declaredMethods) { + if (predicate.test(declaredMethod)) { + methods.add(declaredMethod); + } + } + currentClass = currentClass.getSuperclass(); + } + + return methods.stream() + .sorted(Comparator.comparing(Method::getName)) + .collect(toList()); + } + + private static boolean isPossiblePojoMethod(Method method) { + return !isObjectMethod(method) && + returnsSomething(method) && + isGetterNamed(method) && + hasNoParameters(method) && + isPublic(method); + } + + private static boolean isRecordLike(Method method) { + return !isObjectMethod(method) && + returnsSomething(method) && + hasNoParameters(method) && + isPublic(method); + } + + private static boolean isBooleanGetter(Method method) { + Class returnType = method.getReturnType(); + return isGetterNamed(method) && (returnType.equals(Boolean.class) || returnType.equals(Boolean.TYPE)); + } + + private static boolean hasNoParameters(Method method) { + return method.getParameterCount() == 0; + } + + private static boolean isGetterNamed(Method method) { + String name = method.getName(); + return ((name.startsWith("get") && name.length() > 4) || (name.startsWith("is") && name.length() > 3)); + } + + private static boolean returnsSomething(Method method) { + return !method.getReturnType().equals(Void.class); + } + + private static boolean isPublic(Method method) { + return Modifier.isPublic(method.getModifiers()); + } + + private static boolean isObjectMethod(Method method) { + return method.getDeclaringClass().equals(Object.class); + } + + private static String mkPropertyNameGetter(Method method) { + // + // getFooName becomes fooName + // isFoo becomes foo + // + String name = method.getName(); + if (name.startsWith("get")) { + name = name.substring(3); + } else if (name.startsWith("is")) { + name = name.substring(2); + } + return decapitalize(name); + } + + private static String decapitalize(String name) { + if (name.length() == 0) { + return name; + } + return name.substring(0, 1).toLowerCase() + name.substring(1); + } + + + @VisibleForTesting + static Function mkCallFunction(Class targetClass, String targetMethod, Class targetMethodReturnType) throws Throwable { + MethodHandles.Lookup lookup = MethodHandles.lookup(); + MethodHandle virtualMethodHandle = lookup.findVirtual(targetClass, targetMethod, MethodType.methodType(targetMethodReturnType)); + CallSite site = LambdaMetafactory.metafactory(lookup, + "apply", + MethodType.methodType(Function.class), + MethodType.methodType(Object.class, Object.class), + virtualMethodHandle, + MethodType.methodType(targetMethodReturnType, targetClass)); + @SuppressWarnings("unchecked") + Function getterFunction = (Function) site.getTarget().invokeExact(); + return getterFunction; + } + +} diff --git a/src/main/java/graphql/schema/idl/ArgValueOfAllowedTypeChecker.java b/src/main/java/graphql/schema/idl/ArgValueOfAllowedTypeChecker.java index 83b96fad0c..c9d2498abd 100644 --- a/src/main/java/graphql/schema/idl/ArgValueOfAllowedTypeChecker.java +++ b/src/main/java/graphql/schema/idl/ArgValueOfAllowedTypeChecker.java @@ -1,8 +1,10 @@ package graphql.schema.idl; import graphql.AssertException; +import graphql.GraphQLContext; import graphql.GraphQLError; import graphql.Internal; +import graphql.execution.CoercedVariables; import graphql.language.Argument; import graphql.language.ArrayValue; import graphql.language.Directive; @@ -32,6 +34,7 @@ import org.slf4j.Logger; import java.util.List; +import java.util.Locale; import java.util.Map; import java.util.function.Function; import java.util.stream.Stream; @@ -285,7 +288,7 @@ private void checkArgValueMatchesAllowedListType(List errors, Valu private boolean isArgumentValueScalarLiteral(GraphQLScalarType scalarType, Value instanceValue) { try { - scalarType.getCoercing().parseLiteral(instanceValue); + scalarType.getCoercing().parseLiteral(instanceValue, CoercedVariables.emptyVariables(), GraphQLContext.getDefault(), Locale.getDefault()); return true; } catch (CoercingParseLiteralException ex) { if (logNotSafe.isDebugEnabled()) { diff --git a/src/main/java/graphql/schema/idl/FetchSchemaDirectiveWiring.java b/src/main/java/graphql/schema/idl/FetchSchemaDirectiveWiring.java deleted file mode 100644 index 32898e3f35..0000000000 --- a/src/main/java/graphql/schema/idl/FetchSchemaDirectiveWiring.java +++ /dev/null @@ -1,45 +0,0 @@ -package graphql.schema.idl; - -import graphql.Internal; -import graphql.execution.ValuesResolver; -import graphql.schema.DataFetcher; -import graphql.schema.GraphQLArgument; -import graphql.schema.GraphQLDirective; -import graphql.schema.GraphQLFieldDefinition; -import graphql.schema.PropertyDataFetcher; - -import java.util.List; -import java.util.Optional; - -import static graphql.DirectivesUtil.directiveWithArg; -import static graphql.schema.FieldCoordinates.coordinates; - -/** - * This adds ' @fetch(from : "otherName") ' support so you can rename what property is read for a given field - * - * @deprecated This support introduces a non standard directive and has interfere with some implementations. This is no longer - * installed default and will be removed in a future version - */ -@Internal -@Deprecated -public class FetchSchemaDirectiveWiring implements SchemaDirectiveWiring { - public static final String FETCH = "fetch"; - - @Override - public GraphQLFieldDefinition onField(SchemaDirectiveWiringEnvironment environment) { - GraphQLFieldDefinition field = environment.getElement(); - String fetchName = atFetchFromSupport(field.getName(), field.getDirectives()); - DataFetcher dataFetcher = new PropertyDataFetcher(fetchName); - - environment.getCodeRegistry().dataFetcher(coordinates(environment.getFieldsContainer(), field), dataFetcher); - return field; - } - - - private String atFetchFromSupport(String fieldName, List directives) { - // @fetch(from : "name") - Optional from = directiveWithArg(directives, FETCH, "from"); - return from.map(arg -> (String) ValuesResolver.valueToInternalValue(arg.getArgumentValue(), arg.getType())).orElse(fieldName); - } - -} diff --git a/src/main/java/graphql/schema/idl/ImplementingTypesChecker.java b/src/main/java/graphql/schema/idl/ImplementingTypesChecker.java index 82ce0fdd4d..6f2a22ec10 100644 --- a/src/main/java/graphql/schema/idl/ImplementingTypesChecker.java +++ b/src/main/java/graphql/schema/idl/ImplementingTypesChecker.java @@ -186,8 +186,9 @@ private void checkArgumentConsistency( if (objectArg == null) { errors.add(new MissingInterfaceFieldArgumentsError(typeOfType, objectTypeDef, interfaceTypeDef, objectFieldDef)); } else { - String interfaceArgStr = AstPrinter.printAstCompact(interfaceArg); - String objectArgStr = AstPrinter.printAstCompact(objectArg); + // we need to remove the not relevant applied directives on the argument definitions to compare + String interfaceArgStr = AstPrinter.printAstCompact(interfaceArg.transform(builder -> builder.directives(emptyList()))); + String objectArgStr = AstPrinter.printAstCompact(objectArg.transform(builder -> builder.directives(emptyList()))); if (!interfaceArgStr.equals(objectArgStr)) { errors.add(new InterfaceFieldArgumentRedefinitionError(typeOfType, objectTypeDef, interfaceTypeDef, objectFieldDef, objectArgStr, interfaceArgStr)); } diff --git a/src/main/java/graphql/schema/idl/RuntimeWiring.java b/src/main/java/graphql/schema/idl/RuntimeWiring.java index 2b327d804b..69ba9f6633 100644 --- a/src/main/java/graphql/schema/idl/RuntimeWiring.java +++ b/src/main/java/graphql/schema/idl/RuntimeWiring.java @@ -1,5 +1,6 @@ package graphql.schema.idl; +import graphql.DeprecatedAt; import graphql.PublicApi; import graphql.schema.DataFetcher; import graphql.schema.GraphQLCodeRegistry; @@ -352,7 +353,11 @@ public Builder comparatorRegistry(GraphqlTypeComparatorRegistry comparatorRegist * @param schemaGeneratorPostProcessing the non null schema transformer to add * * @return the runtime wiring builder + * @deprecated This mechanism can be achieved in a better way via {@link graphql.schema.SchemaTransformer} + * after the schema is built */ + @Deprecated + @DeprecatedAt(value = "2022-10-29") public Builder transformer(SchemaGeneratorPostProcessing schemaGeneratorPostProcessing) { this.schemaGeneratorPostProcessings.add(assertNotNull(schemaGeneratorPostProcessing)); return this; diff --git a/src/main/java/graphql/schema/idl/SchemaDirectiveWiringEnvironment.java b/src/main/java/graphql/schema/idl/SchemaDirectiveWiringEnvironment.java index 9440e4579a..dfe09acb43 100644 --- a/src/main/java/graphql/schema/idl/SchemaDirectiveWiringEnvironment.java +++ b/src/main/java/graphql/schema/idl/SchemaDirectiveWiringEnvironment.java @@ -1,5 +1,6 @@ package graphql.schema.idl; +import graphql.DeprecatedAt; import graphql.PublicApi; import graphql.language.NamedNode; import graphql.language.NodeParentTree; @@ -42,6 +43,8 @@ public interface SchemaDirectiveWiringEnvironment getDirectives(); /** @@ -74,6 +79,8 @@ public interface SchemaDirectiveWiringEnvironment schemaTransformers = new ArrayList<>(); + // we check if there are any SchemaDirectiveWiring's in play and if there are // we add this to enable them. By not adding it always, we save unnecessary // schema build traversals if (buildCtx.isDirectiveWiringRequired()) { // handle directive wiring AFTER the schema has been built and hence type references are resolved at callback time - schemaTransformers.add( - new SchemaDirectiveWiringSchemaGeneratorPostProcessing( - buildCtx.getTypeRegistry(), - buildCtx.getWiring(), - buildCtx.getCodeRegistry()) - ); + SchemaDirectiveWiringSchemaGeneratorPostProcessing directiveWiringProcessing = new SchemaDirectiveWiringSchemaGeneratorPostProcessing( + buildCtx.getTypeRegistry(), + buildCtx.getWiring(), + buildCtx.getCodeRegistry()); + graphQLSchema = directiveWiringProcessing.process(graphQLSchema); } - schemaTransformers.addAll(buildCtx.getWiring().getSchemaGeneratorPostProcessings()); - for (SchemaGeneratorPostProcessing postProcessing : schemaTransformers) { + // + // SchemaGeneratorPostProcessing is deprecated but for now we continue to run them + // + for (SchemaGeneratorPostProcessing postProcessing : buildCtx.getWiring().getSchemaGeneratorPostProcessings()) { graphQLSchema = postProcessing.process(graphQLSchema); } return graphQLSchema; diff --git a/src/main/java/graphql/schema/idl/SchemaGeneratorHelper.java b/src/main/java/graphql/schema/idl/SchemaGeneratorHelper.java index 2fcb1bf072..79fdb5493a 100644 --- a/src/main/java/graphql/schema/idl/SchemaGeneratorHelper.java +++ b/src/main/java/graphql/schema/idl/SchemaGeneratorHelper.java @@ -80,8 +80,10 @@ import static graphql.Assert.assertNotNull; import static graphql.Directives.DEPRECATED_DIRECTIVE_DEFINITION; +import static graphql.Directives.IncludeDirective; import static graphql.Directives.NO_LONGER_SUPPORTED; import static graphql.Directives.SPECIFIED_BY_DIRECTIVE_DEFINITION; +import static graphql.Directives.SkipDirective; import static graphql.Directives.SpecifiedByDirective; import static graphql.collect.ImmutableKit.emptyList; import static graphql.introspection.Introspection.DirectiveLocation.ARGUMENT_DEFINITION; @@ -1055,6 +1057,11 @@ Set buildAdditionalDirectiveDefinitions(BuildContext buildCtx) TypeDefinitionRegistry typeRegistry = buildCtx.getTypeRegistry(); for (DirectiveDefinition directiveDefinition : typeRegistry.getDirectiveDefinitions().values()) { + if (IncludeDirective.getName().equals(directiveDefinition.getName()) + || SkipDirective.getName().equals(directiveDefinition.getName())) { + // skip and include directives are added by default to the GraphQLSchema via the GraphQLSchema builder. + continue; + } GraphQLDirective directive = buildDirectiveDefinitionFromAst(buildCtx, directiveDefinition, inputTypeFactory(buildCtx)); buildCtx.addDirectiveDefinition(directive); additionalDirectives.add(directive); @@ -1087,7 +1094,7 @@ private List directivesOf(List> typeDefin private T directivesObserve(BuildContext buildCtx, T directiveContainer) { if (!buildCtx.directiveWiringRequired) { boolean requiresWiring = SchemaGeneratorDirectiveHelper.schemaDirectiveWiringIsRequired(directiveContainer, buildCtx.getTypeRegistry(), buildCtx.getWiring()); - buildCtx.directiveWiringRequired = buildCtx.directiveWiringRequired | requiresWiring; + buildCtx.directiveWiringRequired = buildCtx.directiveWiringRequired || requiresWiring; } return directiveContainer; } diff --git a/src/main/java/graphql/schema/idl/SchemaGeneratorPostProcessing.java b/src/main/java/graphql/schema/idl/SchemaGeneratorPostProcessing.java index 790f537c24..906d187f28 100644 --- a/src/main/java/graphql/schema/idl/SchemaGeneratorPostProcessing.java +++ b/src/main/java/graphql/schema/idl/SchemaGeneratorPostProcessing.java @@ -1,13 +1,19 @@ package graphql.schema.idl; +import graphql.DeprecatedAt; import graphql.PublicSpi; import graphql.schema.GraphQLSchema; /** * These are called by the {@link SchemaGenerator} after a valid schema has been built * and they can then adjust it accordingly with some sort of post processing. + * + * @deprecated This mechanism can be achieved in a better way via {@link graphql.schema.SchemaTransformer} + * after the schema is built */ @PublicSpi +@Deprecated +@DeprecatedAt(value = "2022-10-29") public interface SchemaGeneratorPostProcessing { /** diff --git a/src/main/java/graphql/schema/idl/SchemaParser.java b/src/main/java/graphql/schema/idl/SchemaParser.java index 097e5649f3..8fc0932367 100644 --- a/src/main/java/graphql/schema/idl/SchemaParser.java +++ b/src/main/java/graphql/schema/idl/SchemaParser.java @@ -8,6 +8,7 @@ import graphql.language.SDLDefinition; import graphql.parser.InvalidSyntaxException; import graphql.parser.Parser; +import graphql.parser.ParserEnvironment; import graphql.parser.ParserOptions; import graphql.schema.idl.errors.NonSDLDefinitionError; import graphql.schema.idl.errors.SchemaProblem; @@ -23,6 +24,7 @@ import java.util.Collections; import java.util.List; +import static graphql.parser.ParserEnvironment.newParserEnvironment; import static java.nio.charset.Charset.defaultCharset; /** @@ -116,8 +118,8 @@ private TypeDefinitionRegistry parseImpl(Reader schemaInput, ParserOptions parse if (parseOptions == null) { parseOptions = ParserOptions.getDefaultSdlParserOptions(); } - Parser parser = new Parser(); - Document document = parser.parseDocument(schemaInput, parseOptions); + ParserEnvironment parserEnvironment = newParserEnvironment().document(schemaInput).parserOptions(parseOptions).build(); + Document document = Parser.parse(parserEnvironment); return buildRegistry(document); } catch (InvalidSyntaxException e) { diff --git a/src/main/java/graphql/schema/idl/SchemaPrinter.java b/src/main/java/graphql/schema/idl/SchemaPrinter.java index ea0df6b2e4..e4ae7a7c63 100644 --- a/src/main/java/graphql/schema/idl/SchemaPrinter.java +++ b/src/main/java/graphql/schema/idl/SchemaPrinter.java @@ -2,6 +2,7 @@ import graphql.Assert; import graphql.DirectivesUtil; +import graphql.GraphQLContext; import graphql.PublicApi; import graphql.execution.ValuesResolver; import graphql.language.AstPrinter; @@ -53,6 +54,7 @@ import java.util.Comparator; import java.util.LinkedHashMap; import java.util.List; +import java.util.Locale; import java.util.Map; import java.util.function.Predicate; import java.util.stream.Collectors; @@ -672,7 +674,7 @@ private SchemaElementPrinter inputObjectPrinter() { String astValue = printAst(defaultValue, fd.getType()); out.format(" = %s", astValue); } - out.format(directivesString(GraphQLInputObjectField.class, fd.isDeprecated(), fd)); + out.print(directivesString(GraphQLInputObjectField.class, fd.isDeprecated(), fd)); out.format("\n"); }); out.format("}"); @@ -713,7 +715,7 @@ private void printAsAst(PrintWriter out, TypeDefinition definition, List schemaPrinter() { diff --git a/src/main/java/graphql/schema/impl/GraphQLTypeCollectingVisitor.java b/src/main/java/graphql/schema/impl/GraphQLTypeCollectingVisitor.java index 7da9ab1f88..e6ceb76e3c 100644 --- a/src/main/java/graphql/schema/impl/GraphQLTypeCollectingVisitor.java +++ b/src/main/java/graphql/schema/impl/GraphQLTypeCollectingVisitor.java @@ -10,9 +10,7 @@ import graphql.schema.GraphQLInputObjectField; import graphql.schema.GraphQLInputObjectType; import graphql.schema.GraphQLInterfaceType; -import graphql.schema.GraphQLList; import graphql.schema.GraphQLNamedType; -import graphql.schema.GraphQLNonNull; import graphql.schema.GraphQLObjectType; import graphql.schema.GraphQLScalarType; import graphql.schema.GraphQLSchemaElement; @@ -134,7 +132,7 @@ private void save(String name, GraphQLNamedType type) { /* - From http://facebook.github.io/graphql/#sec-Type-System + From https://spec.graphql.org/October2021/#sec-Type-System All types within a GraphQL schema must have unique names. No two provided types may have the same name. No provided type may have a name which conflicts with any built in types (including Scalar and Introspection types). diff --git a/src/main/java/graphql/schema/impl/SchemaUtil.java b/src/main/java/graphql/schema/impl/SchemaUtil.java index 1f66607938..a1f382d84d 100644 --- a/src/main/java/graphql/schema/impl/SchemaUtil.java +++ b/src/main/java/graphql/schema/impl/SchemaUtil.java @@ -3,6 +3,8 @@ import com.google.common.collect.ImmutableMap; import graphql.Internal; +import graphql.execution.MissingRootTypeException; +import graphql.language.OperationDefinition; import graphql.schema.GraphQLImplementingType; import graphql.schema.GraphQLNamedOutputType; import graphql.schema.GraphQLNamedType; @@ -21,6 +23,11 @@ import java.util.Map; import java.util.TreeMap; +import static graphql.Assert.assertShouldNeverHappen; +import static graphql.language.OperationDefinition.Operation.MUTATION; +import static graphql.language.OperationDefinition.Operation.QUERY; +import static graphql.language.OperationDefinition.Operation.SUBSCRIPTION; + @Internal public class SchemaUtil { @@ -96,4 +103,29 @@ public static void replaceTypeReferences(GraphQLSchema schema) { SchemaTraverser schemaTraverser = new SchemaTraverser(schemaElement -> schemaElement.getChildrenWithTypeReferences().getChildrenAsList()); schemaTraverser.depthFirst(new GraphQLTypeResolvingVisitor(typeMap), roots); } + + public static GraphQLObjectType getOperationRootType(GraphQLSchema graphQLSchema, OperationDefinition operationDefinition) { + OperationDefinition.Operation operation = operationDefinition.getOperation(); + if (operation == MUTATION) { + GraphQLObjectType mutationType = graphQLSchema.getMutationType(); + if (mutationType == null) { + throw new MissingRootTypeException("Schema is not configured for mutations.", operationDefinition.getSourceLocation()); + } + return mutationType; + } else if (operation == QUERY) { + GraphQLObjectType queryType = graphQLSchema.getQueryType(); + if (queryType == null) { + throw new MissingRootTypeException("Schema does not define the required query root type.", operationDefinition.getSourceLocation()); + } + return queryType; + } else if (operation == SUBSCRIPTION) { + GraphQLObjectType subscriptionType = graphQLSchema.getSubscriptionType(); + if (subscriptionType == null) { + throw new MissingRootTypeException("Schema is not configured for subscriptions.", operationDefinition.getSourceLocation()); + } + return subscriptionType; + } else { + return assertShouldNeverHappen("Unhandled case. An extra operation enum has been added without code support"); + } + } } diff --git a/src/main/java/graphql/schema/transform/FieldVisibilitySchemaTransformation.java b/src/main/java/graphql/schema/transform/FieldVisibilitySchemaTransformation.java index a290258838..303b81b9c8 100644 --- a/src/main/java/graphql/schema/transform/FieldVisibilitySchemaTransformation.java +++ b/src/main/java/graphql/schema/transform/FieldVisibilitySchemaTransformation.java @@ -1,10 +1,12 @@ package graphql.schema.transform; +import com.google.common.collect.ImmutableList; import graphql.PublicApi; import graphql.schema.GraphQLEnumType; import graphql.schema.GraphQLFieldDefinition; import graphql.schema.GraphQLImplementingType; import graphql.schema.GraphQLInputObjectField; +import graphql.schema.GraphQLInputObjectType; import graphql.schema.GraphQLInterfaceType; import graphql.schema.GraphQLNamedSchemaElement; import graphql.schema.GraphQLNamedType; @@ -60,7 +62,7 @@ public final GraphQLSchema apply(GraphQLSchema schema) { Set markedForRemovalTypes = new HashSet<>(); // query, mutation, and subscription types should not be removed - final Set protectedTypeNames = getRootTypes(schema).stream() + final Set protectedTypeNames = getOperationTypes(schema).stream() .map(GraphQLObjectType::getName) .collect(Collectors.toSet()); @@ -216,6 +218,7 @@ public TraversalControl visitGraphQLType(GraphQLSchemaElement node, !observedAfterTransform.contains(node) && (node instanceof GraphQLObjectType || node instanceof GraphQLEnumType || + node instanceof GraphQLInputObjectType || node instanceof GraphQLInterfaceType || node instanceof GraphQLUnionType)) { @@ -250,12 +253,21 @@ public TraversalControl visitGraphQLType(GraphQLSchemaElement node, } } - private List getRootTypes(GraphQLSchema schema) { + private List getRootTypes(GraphQLSchema schema) { + return ImmutableList.builder() + .addAll(getOperationTypes(schema)) + // Include directive definitions as roots, since they won't be removed in the filtering process. + // Some types (enums, input types, etc.) might be reachable only by directive definitions (and + // not by other types or fields). + .addAll(schema.getDirectives()) + .build(); + } + + private List getOperationTypes(GraphQLSchema schema) { return Stream.of( schema.getQueryType(), schema.getSubscriptionType(), schema.getMutationType() ).filter(Objects::nonNull).collect(Collectors.toList()); } - } diff --git a/src/main/java/graphql/schema/usage/SchemaUsage.java b/src/main/java/graphql/schema/usage/SchemaUsage.java index 0caff84459..25ed7fb24c 100644 --- a/src/main/java/graphql/schema/usage/SchemaUsage.java +++ b/src/main/java/graphql/schema/usage/SchemaUsage.java @@ -47,6 +47,8 @@ public class SchemaUsage { private final Map> interfaceImplementors; private final Map> elementBackReferences; + private final Map> unionReferences; + private SchemaUsage(Builder builder) { this.fieldReferenceCounts = ImmutableMap.copyOf(builder.fieldReferenceCounts); this.inputFieldReferenceCounts = ImmutableMap.copyOf(builder.inputFieldReferenceCounts); @@ -57,6 +59,7 @@ private SchemaUsage(Builder builder) { this.directiveReferenceCount = ImmutableMap.copyOf(builder.directiveReferenceCount); this.interfaceImplementors = ImmutableMap.copyOf(builder.interfaceImplementors); this.elementBackReferences = ImmutableMap.copyOf(builder.elementBackReferences); + this.unionReferences = ImmutableMap.copyOf(builder.unionReferences); } /** @@ -225,6 +228,13 @@ private boolean isReferencedImpl(GraphQLSchema schema, String elementName, Set unionContainers = unionReferences.getOrDefault(type.getName(), emptySet()); + for (String unionContainer : unionContainers) { + if (isReferencedImpl(schema, unionContainer, pathSoFar)) { + return true; + } + } } return false; } @@ -249,6 +259,8 @@ static class Builder { Map unionReferenceCount = new LinkedHashMap<>(); Map directiveReferenceCount = new LinkedHashMap<>(); Map> interfaceImplementors = new LinkedHashMap<>(); + + Map> unionReferences = new LinkedHashMap<>(); Map> elementBackReferences = new LinkedHashMap<>(); SchemaUsage build() { diff --git a/src/main/java/graphql/schema/usage/SchemaUsageSupport.java b/src/main/java/graphql/schema/usage/SchemaUsageSupport.java index cb41f69992..e540534d3b 100644 --- a/src/main/java/graphql/schema/usage/SchemaUsageSupport.java +++ b/src/main/java/graphql/schema/usage/SchemaUsageSupport.java @@ -135,6 +135,7 @@ public TraversalControl visitGraphQLUnionType(GraphQLUnionType unionType, Traver List members = unionType.getTypes(); for (GraphQLNamedOutputType member : members) { builder.unionReferenceCount.compute(member.getName(), incCount()); + builder.unionReferences.computeIfAbsent(member.getName(), k -> new HashSet<>()).add(unionType.getName()); recordBackReference(unionType, member); } diff --git a/src/main/java/graphql/schema/validation/AppliedDirectiveArgumentsAreValid.java b/src/main/java/graphql/schema/validation/AppliedDirectiveArgumentsAreValid.java index af9beb9aab..82e59e2785 100644 --- a/src/main/java/graphql/schema/validation/AppliedDirectiveArgumentsAreValid.java +++ b/src/main/java/graphql/schema/validation/AppliedDirectiveArgumentsAreValid.java @@ -1,5 +1,6 @@ package graphql.schema.validation; +import graphql.GraphQLContext; import graphql.Internal; import graphql.execution.NonNullableValueCoercedAsNullException; import graphql.execution.ValuesResolver; @@ -16,6 +17,8 @@ import graphql.util.TraverserContext; import graphql.validation.ValidationUtil; +import java.util.Locale; + import static java.lang.String.format; @Internal @@ -44,10 +47,10 @@ private void checkArgument(GraphQLDirective directive, GraphQLArgument argument, InputValueWithState argumentValue = argument.getArgumentValue(); boolean invalid = false; if (argumentValue.isLiteral() && - !validationUtil.isValidLiteralValue((Value) argumentValue.getValue(), argument.getType(), schema)) { + !validationUtil.isValidLiteralValue((Value) argumentValue.getValue(), argument.getType(), schema, GraphQLContext.getDefault(), Locale.getDefault())) { invalid = true; } else if (argumentValue.isExternal() && - !isValidExternalValue(schema, argumentValue.getValue(), argument.getType())) { + !isValidExternalValue(schema, argumentValue.getValue(), argument.getType(), GraphQLContext.getDefault(), Locale.getDefault())) { invalid = true; } if (invalid) { @@ -56,9 +59,9 @@ private void checkArgument(GraphQLDirective directive, GraphQLArgument argument, } } - private boolean isValidExternalValue(GraphQLSchema schema, Object externalValue, GraphQLInputType type) { + private boolean isValidExternalValue(GraphQLSchema schema, Object externalValue, GraphQLInputType type, GraphQLContext graphQLContext, Locale locale) { try { - ValuesResolver.externalValueToInternalValue(schema.getCodeRegistry().getFieldVisibility(), externalValue, type); + ValuesResolver.externalValueToInternalValue(schema.getCodeRegistry().getFieldVisibility(), externalValue, type, graphQLContext, locale); return true; } catch (CoercingParseValueException | NonNullableValueCoercedAsNullException e) { return false; diff --git a/src/main/java/graphql/schema/validation/DefaultValuesAreValid.java b/src/main/java/graphql/schema/validation/DefaultValuesAreValid.java index ad1a15b021..4527973461 100644 --- a/src/main/java/graphql/schema/validation/DefaultValuesAreValid.java +++ b/src/main/java/graphql/schema/validation/DefaultValuesAreValid.java @@ -1,5 +1,6 @@ package graphql.schema.validation; +import graphql.GraphQLContext; import graphql.execution.NonNullableValueCoercedAsNullException; import graphql.execution.ValuesResolver; import graphql.language.Value; @@ -16,6 +17,8 @@ import graphql.util.TraverserContext; import graphql.validation.ValidationUtil; +import java.util.Locale; + import static graphql.schema.GraphQLTypeUtil.simplePrint; import static java.lang.String.format; @@ -23,6 +26,9 @@ public class DefaultValuesAreValid extends GraphQLTypeVisitorStub { private ValidationUtil validationUtil = new ValidationUtil(); + private final GraphQLContext graphQLContext = GraphQLContext.getDefault(); + + @Override public TraversalControl visitGraphQLInputObjectType(GraphQLInputObjectType node, TraverserContext context) { return super.visitGraphQLInputObjectType(node, context); @@ -38,10 +44,10 @@ public TraversalControl visitGraphQLInputObjectField(GraphQLInputObjectField inp InputValueWithState defaultValue = inputObjectField.getInputFieldDefaultValue(); boolean invalid = false; if (defaultValue.isLiteral() && - !validationUtil.isValidLiteralValue((Value) defaultValue.getValue(), inputObjectField.getType(), schema)) { + !validationUtil.isValidLiteralValue((Value) defaultValue.getValue(), inputObjectField.getType(), schema, graphQLContext, Locale.getDefault())) { invalid = true; } else if (defaultValue.isExternal() && - !isValidExternalValue(schema, defaultValue.getValue(), inputObjectField.getType())) { + !isValidExternalValue(schema, defaultValue.getValue(), inputObjectField.getType(), graphQLContext)) { invalid = true; } if (invalid) { @@ -61,10 +67,10 @@ public TraversalControl visitGraphQLArgument(GraphQLArgument argument, Traverser InputValueWithState defaultValue = argument.getArgumentDefaultValue(); boolean invalid = false; if (defaultValue.isLiteral() && - !validationUtil.isValidLiteralValue((Value) defaultValue.getValue(), argument.getType(), schema)) { + !validationUtil.isValidLiteralValue((Value) defaultValue.getValue(), argument.getType(), schema, graphQLContext, Locale.getDefault())) { invalid = true; } else if (defaultValue.isExternal() && - !isValidExternalValue(schema, defaultValue.getValue(), argument.getType())) { + !isValidExternalValue(schema, defaultValue.getValue(), argument.getType(), graphQLContext)) { invalid = true; } if (invalid) { @@ -74,9 +80,9 @@ public TraversalControl visitGraphQLArgument(GraphQLArgument argument, Traverser return TraversalControl.CONTINUE; } - private boolean isValidExternalValue(GraphQLSchema schema, Object externalValue, GraphQLInputType type) { + private boolean isValidExternalValue(GraphQLSchema schema, Object externalValue, GraphQLInputType type, GraphQLContext graphQLContext) { try { - ValuesResolver.externalValueToInternalValue(schema.getCodeRegistry().getFieldVisibility(), externalValue, type); + ValuesResolver.externalValueToInternalValue(schema.getCodeRegistry().getFieldVisibility(), externalValue, type, graphQLContext, Locale.getDefault()); return true; } catch (CoercingParseValueException | NonNullableValueCoercedAsNullException e) { return false; diff --git a/src/main/java/graphql/schema/validation/TypeAndFieldRule.java b/src/main/java/graphql/schema/validation/TypeAndFieldRule.java index 4cb8d3a292..84a768479d 100644 --- a/src/main/java/graphql/schema/validation/TypeAndFieldRule.java +++ b/src/main/java/graphql/schema/validation/TypeAndFieldRule.java @@ -23,7 +23,6 @@ import graphql.util.TraversalControl; import graphql.util.TraverserContext; -import java.util.Collections; import java.util.HashSet; import java.util.List; import java.util.Set; diff --git a/src/main/java/graphql/schema/validation/TypesImplementInterfaces.java b/src/main/java/graphql/schema/validation/TypesImplementInterfaces.java index f7ec36169f..2717ed8e46 100644 --- a/src/main/java/graphql/schema/validation/TypesImplementInterfaces.java +++ b/src/main/java/graphql/schema/validation/TypesImplementInterfaces.java @@ -1,5 +1,6 @@ package graphql.schema.validation; +import graphql.GraphQLContext; import graphql.Internal; import graphql.execution.ValuesResolver; import graphql.language.Value; @@ -20,6 +21,7 @@ import java.util.HashMap; import java.util.List; +import java.util.Locale; import java.util.Map; import java.util.Objects; import java.util.stream.Collectors; @@ -151,8 +153,8 @@ private void checkFieldArgumentEquivalence(GraphQLImplementingType implementingT same = false; } if (objectArg.hasSetDefaultValue() && interfaceArg.hasSetDefaultValue()) { - Value objectDefaultValue = ValuesResolver.valueToLiteral(objectArg.getArgumentDefaultValue(), objectArg.getType()); - Value interfaceDefaultValue = ValuesResolver.valueToLiteral(interfaceArg.getArgumentDefaultValue(), interfaceArg.getType()); + Value objectDefaultValue = ValuesResolver.valueToLiteral(objectArg.getArgumentDefaultValue(), objectArg.getType(), GraphQLContext.getDefault(), Locale.getDefault()); + Value interfaceDefaultValue = ValuesResolver.valueToLiteral(interfaceArg.getArgumentDefaultValue(), interfaceArg.getType(), GraphQLContext.getDefault(), Locale.getDefault()); if (!Objects.equals(printAst(objectDefaultValue), printAst(interfaceDefaultValue))) { same = false; } diff --git a/src/main/java/graphql/schema/visibility/NoIntrospectionGraphqlFieldVisibility.java b/src/main/java/graphql/schema/visibility/NoIntrospectionGraphqlFieldVisibility.java index 2fc2fe6f00..604e794114 100644 --- a/src/main/java/graphql/schema/visibility/NoIntrospectionGraphqlFieldVisibility.java +++ b/src/main/java/graphql/schema/visibility/NoIntrospectionGraphqlFieldVisibility.java @@ -10,8 +10,8 @@ /** * This field visibility will prevent Introspection queries from being performed. Technically this puts your - * system in contravention of the specification - http://facebook.github.io/graphql/#sec-Introspection but some - * production systems want this lock down in place. + * system in contravention of the specification + * but some production systems want this lock down in place. */ @PublicApi public class NoIntrospectionGraphqlFieldVisibility implements GraphqlFieldVisibility { diff --git a/src/main/java/graphql/util/Anonymizer.java b/src/main/java/graphql/util/Anonymizer.java index bb5fc69d81..34553d8777 100644 --- a/src/main/java/graphql/util/Anonymizer.java +++ b/src/main/java/graphql/util/Anonymizer.java @@ -2,6 +2,7 @@ import graphql.AssertException; import graphql.Directives; +import graphql.GraphQLContext; import graphql.PublicApi; import graphql.Scalars; import graphql.analysis.QueryTraverser; @@ -42,8 +43,8 @@ import graphql.language.VariableDefinition; import graphql.language.VariableReference; import graphql.parser.Parser; -import graphql.schema.GraphQLAppliedDirectiveArgument; import graphql.schema.GraphQLAppliedDirective; +import graphql.schema.GraphQLAppliedDirectiveArgument; import graphql.schema.GraphQLArgument; import graphql.schema.GraphQLCodeRegistry; import graphql.schema.GraphQLDirective; @@ -78,11 +79,11 @@ import java.math.BigInteger; import java.util.ArrayList; -import java.util.Collections; import java.util.HashMap; import java.util.LinkedHashMap; import java.util.LinkedHashSet; import java.util.List; +import java.util.Locale; import java.util.Map; import java.util.Optional; import java.util.Set; @@ -91,6 +92,7 @@ import java.util.function.Consumer; import static graphql.Assert.assertNotNull; +import static graphql.parser.ParserEnvironment.newParserEnvironment; import static graphql.schema.GraphQLArgument.newArgument; import static graphql.schema.GraphQLTypeUtil.unwrapNonNull; import static graphql.schema.GraphQLTypeUtil.unwrapNonNullAs; @@ -184,12 +186,12 @@ public TraversalControl visitGraphQLArgument(GraphQLArgument graphQLArgument, Tr GraphQLArgument newElement = graphQLArgument.transform(builder -> { builder.name(newName).description(null).definition(null); if (graphQLArgument.hasSetDefaultValue()) { - Value defaultValueLiteral = ValuesResolver.valueToLiteral(graphQLArgument.getArgumentDefaultValue(), graphQLArgument.getType()); + Value defaultValueLiteral = ValuesResolver.valueToLiteral(graphQLArgument.getArgumentDefaultValue(), graphQLArgument.getType(), GraphQLContext.getDefault(), Locale.getDefault()); builder.defaultValueLiteral(replaceValue(defaultValueLiteral, graphQLArgument.getType(), newNameMap, defaultStringValueCounter, defaultIntValueCounter)); } if (graphQLArgument.hasSetValue()) { - Value valueLiteral = ValuesResolver.valueToLiteral(graphQLArgument.getArgumentValue(), graphQLArgument.getType()); + Value valueLiteral = ValuesResolver.valueToLiteral(graphQLArgument.getArgumentValue(), graphQLArgument.getType(), GraphQLContext.getDefault(), Locale.getDefault()); builder.valueLiteral(replaceValue(valueLiteral, graphQLArgument.getType(), newNameMap, defaultStringValueCounter, defaultIntValueCounter)); } }); @@ -205,7 +207,7 @@ public TraversalControl visitGraphQLAppliedDirectiveArgument(GraphQLAppliedDirec GraphQLAppliedDirectiveArgument newElement = graphQLArgument.transform(builder -> { builder.name(newName).description(null).definition(null); if (graphQLArgument.hasSetValue()) { - Value valueLiteral = ValuesResolver.valueToLiteral(graphQLArgument.getArgumentValue(), graphQLArgument.getType()); + Value valueLiteral = ValuesResolver.valueToLiteral(graphQLArgument.getArgumentValue(), graphQLArgument.getType(), GraphQLContext.getDefault(), Locale.getDefault()); builder.valueLiteral(replaceValue(valueLiteral, graphQLArgument.getType(), newNameMap, defaultStringValueCounter, defaultIntValueCounter)); } }); @@ -307,7 +309,7 @@ public TraversalControl visitGraphQLInputObjectField(GraphQLInputObjectField gra Value defaultValue = null; if (graphQLInputObjectField.hasSetDefaultValue()) { - defaultValue = ValuesResolver.valueToLiteral(graphQLInputObjectField.getInputFieldDefaultValue(), graphQLInputObjectField.getType()); + defaultValue = ValuesResolver.valueToLiteral(graphQLInputObjectField.getInputFieldDefaultValue(), graphQLInputObjectField.getType(), GraphQLContext.getDefault(), Locale.getDefault()); defaultValue = replaceValue(defaultValue, graphQLInputObjectField.getType(), newNameMap, defaultStringValueCounter, defaultIntValueCounter); } @@ -714,7 +716,8 @@ private static String rewriteQuery(String query, GraphQLSchema schema, Map astNodeToNewName = new LinkedHashMap<>(); Map variableNames = new LinkedHashMap<>(); Map fieldToFieldDefinition = new LinkedHashMap<>(); - Document document = new Parser().parseDocument(query); + + Document document = Parser.parse(newParserEnvironment().document(query).build()); assertUniqueOperation(document); QueryTraverser queryTraverser = QueryTraverser.newQueryTraverser().document(document).schema(schema).variables(variables).build(); queryTraverser.visitDepthFirst(new QueryVisitor() { @@ -794,8 +797,6 @@ public TraversalControl visitArgument(QueryVisitorFieldArgumentEnvironment envir } }); - AtomicInteger stringValueCounter = new AtomicInteger(1); - AtomicInteger intValueCounter = new AtomicInteger(1); AstTransformer astTransformer = new AstTransformer(); AtomicInteger aliasCounter = new AtomicInteger(1); AtomicInteger defaultStringValueCounter = new AtomicInteger(1); diff --git a/src/main/java/graphql/util/Breadcrumb.java b/src/main/java/graphql/util/Breadcrumb.java index cae637efdc..ad1415a00a 100644 --- a/src/main/java/graphql/util/Breadcrumb.java +++ b/src/main/java/graphql/util/Breadcrumb.java @@ -3,6 +3,7 @@ import graphql.PublicApi; import java.util.Objects; +import java.util.StringJoiner; /** * A specific {@link NodeLocation} inside a node. This means {@link #getNode()} returns a Node which has a child @@ -47,9 +48,16 @@ public boolean equals(Object o) { @Override public int hashCode() { int result = 1; - result = 31 * result + Objects.hashCode(node); + result = 31 * result + Objects.hashCode(node); result = 31 * result + Objects.hashCode(location); return result; } + @Override + public String toString() { + return new StringJoiner(", ", "[", "]") + .add("" + location) + .add("" + node) + .toString(); + } } diff --git a/src/main/java/graphql/util/FpKit.java b/src/main/java/graphql/util/FpKit.java index ce164b6595..86ea96374b 100644 --- a/src/main/java/graphql/util/FpKit.java +++ b/src/main/java/graphql/util/FpKit.java @@ -49,6 +49,16 @@ public static Map> groupingBy(Collection return list.stream().collect(Collectors.groupingBy(function, LinkedHashMap::new, mapping(Function.identity(), ImmutableList.toImmutableList()))); } + public static Map> filterAndGroupingBy(Collection list, + Predicate predicate, + Function function) { + return list.stream().filter(predicate).collect(Collectors.groupingBy(function, LinkedHashMap::new, mapping(Function.identity(), ImmutableList.toImmutableList()))); + } + + public static Map> groupingBy(Stream stream, Function function) { + return stream.collect(Collectors.groupingBy(function, LinkedHashMap::new, mapping(Function.identity(), ImmutableList.toImmutableList()))); + } + public static Map groupingByUniqueKey(Collection list, Function keyFunction) { return list.stream().collect(Collectors.toMap( keyFunction, @@ -58,6 +68,15 @@ public static Map groupingByUniqueKey(Collection list, ); } + public static Map groupingByUniqueKey(Stream stream, Function keyFunction) { + return stream.collect(Collectors.toMap( + keyFunction, + identity(), + throwingMerger(), + LinkedHashMap::new) + ); + } + private static BinaryOperator throwingMerger() { return (u, v) -> { throw new IllegalStateException(String.format("Duplicate key %s", u)); @@ -333,6 +352,7 @@ public static Supplier interThreadMemoize(Supplier delegate) { /** * Faster set intersection. * + * @param for two * @param set1 first set * @param set2 second set * @return intersection set diff --git a/src/main/java/graphql/util/NodeLocation.java b/src/main/java/graphql/util/NodeLocation.java index 61851b0b6f..8a1f9f10bc 100644 --- a/src/main/java/graphql/util/NodeLocation.java +++ b/src/main/java/graphql/util/NodeLocation.java @@ -33,7 +33,7 @@ public int getIndex() { @Override public String toString() { - return "NodeLocation{" + + return "{" + "name='" + name + '\'' + ", index=" + index + '}'; diff --git a/src/main/java/graphql/util/TreeParallelTransformer.java b/src/main/java/graphql/util/TreeParallelTransformer.java index d59151dd94..0102af2eff 100644 --- a/src/main/java/graphql/util/TreeParallelTransformer.java +++ b/src/main/java/graphql/util/TreeParallelTransformer.java @@ -5,7 +5,6 @@ import java.util.ArrayList; import java.util.Collection; -import java.util.Collections; import java.util.HashMap; import java.util.LinkedHashMap; import java.util.LinkedList; diff --git a/src/main/java/graphql/util/TreeTransformer.java b/src/main/java/graphql/util/TreeTransformer.java index ebfc9cc950..970b0eac48 100644 --- a/src/main/java/graphql/util/TreeTransformer.java +++ b/src/main/java/graphql/util/TreeTransformer.java @@ -4,7 +4,6 @@ import graphql.PublicApi; import graphql.collect.ImmutableKit; -import java.util.Collections; import java.util.LinkedList; import java.util.List; import java.util.Map; diff --git a/src/main/java/graphql/validation/TraversalContext.java b/src/main/java/graphql/validation/TraversalContext.java index f9a157941e..dfd3e4920d 100644 --- a/src/main/java/graphql/validation/TraversalContext.java +++ b/src/main/java/graphql/validation/TraversalContext.java @@ -2,7 +2,6 @@ import graphql.Assert; -import graphql.DirectivesUtil; import graphql.Internal; import graphql.execution.TypeFromAST; import graphql.language.Argument; diff --git a/src/main/java/graphql/validation/ValidationContext.java b/src/main/java/graphql/validation/ValidationContext.java index ad94f603ee..2646afcdbb 100644 --- a/src/main/java/graphql/validation/ValidationContext.java +++ b/src/main/java/graphql/validation/ValidationContext.java @@ -1,6 +1,7 @@ package graphql.validation; +import graphql.GraphQLContext; import graphql.Internal; import graphql.i18n.I18n; import graphql.language.Definition; @@ -16,6 +17,7 @@ import java.util.LinkedHashMap; import java.util.List; +import java.util.Locale; import java.util.Map; @Internal @@ -27,12 +29,14 @@ public class ValidationContext { private final TraversalContext traversalContext; private final Map fragmentDefinitionMap = new LinkedHashMap<>(); private final I18n i18n; + private final GraphQLContext graphQLContext; public ValidationContext(GraphQLSchema schema, Document document, I18n i18n) { this.schema = schema; this.document = document; this.traversalContext = new TraversalContext(schema); this.i18n = i18n; + this.graphQLContext = GraphQLContext.newContext().of(Locale.class, i18n.getLocale()).build(); buildFragmentMap(); } @@ -92,6 +96,10 @@ public I18n getI18n() { return i18n; } + public GraphQLContext getGraphQLContext() { + return graphQLContext; + } + /** * Creates an I18N message using the key and arguments * diff --git a/src/main/java/graphql/validation/ValidationError.java b/src/main/java/graphql/validation/ValidationError.java index 2b257b047b..841db1c17a 100644 --- a/src/main/java/graphql/validation/ValidationError.java +++ b/src/main/java/graphql/validation/ValidationError.java @@ -1,6 +1,8 @@ package graphql.validation; +import com.google.common.collect.ImmutableMap; +import graphql.DeprecatedAt; import graphql.ErrorType; import graphql.GraphQLError; import graphql.GraphqlErrorHelper; @@ -11,24 +13,26 @@ import java.util.Collections; import java.util.List; import java.util.Map; +import java.util.stream.Collectors; @PublicApi public class ValidationError implements GraphQLError { - private final String message; private final List locations = new ArrayList<>(); private final String description; private final ValidationErrorClassification validationErrorType; - private final List queryPath; - private final Map extensions; + private final List queryPath = new ArrayList<>(); + private final ImmutableMap extensions; @Deprecated + @DeprecatedAt("2022-07-10") public ValidationError(ValidationErrorClassification validationErrorType) { this(newValidationError() .validationErrorType(validationErrorType)); } @Deprecated + @DeprecatedAt("2022-07-10") public ValidationError(ValidationErrorClassification validationErrorType, SourceLocation sourceLocation, String description) { this(newValidationError() .validationErrorType(validationErrorType) @@ -37,6 +41,7 @@ public ValidationError(ValidationErrorClassification validationErrorType, Source } @Deprecated + @DeprecatedAt("2022-07-10") public ValidationError(ValidationErrorType validationErrorType, SourceLocation sourceLocation, String description, List queryPath) { this(newValidationError() .validationErrorType(validationErrorType) @@ -46,6 +51,7 @@ public ValidationError(ValidationErrorType validationErrorType, SourceLocation s } @Deprecated + @DeprecatedAt("2022-07-10") public ValidationError(ValidationErrorType validationErrorType, List sourceLocations, String description) { this(newValidationError() .validationErrorType(validationErrorType) @@ -54,6 +60,7 @@ public ValidationError(ValidationErrorType validationErrorType, List sourceLocations, String description, List queryPath) { this(newValidationError() .validationErrorType(validationErrorType) @@ -64,13 +71,16 @@ public ValidationError(ValidationErrorType validationErrorType, List getExtensions() { @Override public String toString() { + String extensionsString = ""; + + if (extensions.size() > 0) { + extensionsString = extensions + .keySet() + .stream() + .map(key -> key + "=" + extensions.get(key)) + .collect(Collectors.joining(", ")); + } + return "ValidationError{" + "validationErrorType=" + validationErrorType + ", queryPath=" + queryPath + - ", message=" + message + + ", message=" + description + ", locations=" + locations + ", description='" + description + '\'' + + ", extensions=[" + extensionsString + ']' + '}'; } diff --git a/src/main/java/graphql/validation/ValidationUtil.java b/src/main/java/graphql/validation/ValidationUtil.java index a1bbe00ccb..413b5887fc 100644 --- a/src/main/java/graphql/validation/ValidationUtil.java +++ b/src/main/java/graphql/validation/ValidationUtil.java @@ -3,8 +3,10 @@ import com.google.common.collect.ImmutableSet; import graphql.Assert; +import graphql.GraphQLContext; import graphql.GraphQLError; import graphql.Internal; +import graphql.execution.CoercedVariables; import graphql.language.ArrayValue; import graphql.language.ListType; import graphql.language.NonNullType; @@ -28,6 +30,7 @@ import java.util.LinkedHashMap; import java.util.List; +import java.util.Locale; import java.util.Map; import java.util.Optional; import java.util.Set; @@ -74,7 +77,7 @@ protected void handleFieldNotValidError(ObjectField objectField, GraphQLInputObj protected void handleFieldNotValidError(Value value, GraphQLType type, int index) { } - public boolean isValidLiteralValue(Value value, GraphQLType type, GraphQLSchema schema) { + public boolean isValidLiteralValue(Value value, GraphQLType type, GraphQLSchema schema, GraphQLContext graphQLContext, Locale locale) { if (value == null || value instanceof NullValue) { boolean valid = !(isNonNull(type)); if (!valid) { @@ -86,46 +89,46 @@ public boolean isValidLiteralValue(Value value, GraphQLType type, GraphQLSche return true; } if (isNonNull(type)) { - return isValidLiteralValue(value, unwrapOne(type), schema); + return isValidLiteralValue(value, unwrapOne(type), schema, graphQLContext, locale); } if (type instanceof GraphQLScalarType) { - Optional invalid = parseLiteral(value, ((GraphQLScalarType) type).getCoercing()); + Optional invalid = parseLiteral(value, ((GraphQLScalarType) type).getCoercing(), graphQLContext, locale); invalid.ifPresent(graphQLError -> handleScalarError(value, (GraphQLScalarType) type, graphQLError)); return !invalid.isPresent(); } if (type instanceof GraphQLEnumType) { - Optional invalid = parseLiteralEnum(value, (GraphQLEnumType) type); + Optional invalid = parseLiteralEnum(value, (GraphQLEnumType) type, graphQLContext, locale); invalid.ifPresent(graphQLError -> handleEnumError(value, (GraphQLEnumType) type, graphQLError)); return !invalid.isPresent(); } if (isList(type)) { - return isValidLiteralValue(value, (GraphQLList) type, schema); + return isValidLiteralValue(value, (GraphQLList) type, schema, graphQLContext, locale); } - return type instanceof GraphQLInputObjectType && isValidLiteralValue(value, (GraphQLInputObjectType) type, schema); + return type instanceof GraphQLInputObjectType && isValidLiteralValue(value, (GraphQLInputObjectType) type, schema, graphQLContext, locale); } - private Optional parseLiteralEnum(Value value, GraphQLEnumType graphQLEnumType) { + private Optional parseLiteralEnum(Value value, GraphQLEnumType graphQLEnumType, GraphQLContext graphQLContext, Locale locale) { try { - graphQLEnumType.parseLiteral(value); + graphQLEnumType.parseLiteral(value, graphQLContext, locale); return Optional.empty(); } catch (CoercingParseLiteralException e) { return Optional.of(e); } } - private Optional parseLiteral(Value value, Coercing coercing) { + private Optional parseLiteral(Value value, Coercing coercing, GraphQLContext graphQLContext, Locale locale) { try { - coercing.parseLiteral(value); + coercing.parseLiteral(value, CoercedVariables.emptyVariables(), graphQLContext, locale); return Optional.empty(); } catch (CoercingParseLiteralException e) { return Optional.of(e); } } - private boolean isValidLiteralValue(Value value, GraphQLInputObjectType type, GraphQLSchema schema) { + boolean isValidLiteralValue(Value value, GraphQLInputObjectType type, GraphQLSchema schema, GraphQLContext graphQLContext, Locale locale) { if (!(value instanceof ObjectValue)) { handleNotObjectError(value, type); return false; @@ -147,7 +150,7 @@ private boolean isValidLiteralValue(Value value, GraphQLInputObjectType type, handleExtraFieldError(value, type, objectField); return false; } - if (!isValidLiteralValue(objectField.getValue(), inputObjectField.getType(), schema)) { + if (!isValidLiteralValue(objectField.getValue(), inputObjectField.getType(), schema, graphQLContext, locale)) { handleFieldNotValidError(objectField, type); return false; } @@ -172,19 +175,19 @@ private Map fieldMap(ObjectValue objectValue) { return result; } - private boolean isValidLiteralValue(Value value, GraphQLList type, GraphQLSchema schema) { + private boolean isValidLiteralValue(Value value, GraphQLList type, GraphQLSchema schema, GraphQLContext graphQLContext, Locale locale) { GraphQLType wrappedType = type.getWrappedType(); if (value instanceof ArrayValue) { List values = ((ArrayValue) value).getValues(); for (int i = 0; i < values.size(); i++) { - if (!isValidLiteralValue(values.get(i), wrappedType, schema)) { + if (!isValidLiteralValue(values.get(i), wrappedType, schema, graphQLContext, locale)) { handleFieldNotValidError(values.get(i), wrappedType, i); return false; } } return true; } else { - return isValidLiteralValue(value, wrappedType, schema); + return isValidLiteralValue(value, wrappedType, schema, graphQLContext, locale); } } diff --git a/src/main/java/graphql/validation/rules/ArgumentsOfCorrectType.java b/src/main/java/graphql/validation/rules/ArgumentsOfCorrectType.java index 0f942bdfc8..21e85a2129 100644 --- a/src/main/java/graphql/validation/rules/ArgumentsOfCorrectType.java +++ b/src/main/java/graphql/validation/rules/ArgumentsOfCorrectType.java @@ -10,6 +10,8 @@ import graphql.validation.ValidationError; import graphql.validation.ValidationErrorCollector; +import java.util.Locale; + import static graphql.validation.ValidationErrorType.WrongType; @Internal @@ -26,7 +28,7 @@ public void checkArgument(Argument argument) { return; } ArgumentValidationUtil validationUtil = new ArgumentValidationUtil(argument); - if (!validationUtil.isValidLiteralValue(argument.getValue(), fieldArgument.getType(), getValidationContext().getSchema())) { + if (!validationUtil.isValidLiteralValue(argument.getValue(), fieldArgument.getType(), getValidationContext().getSchema(), getValidationContext().getGraphQLContext(), Locale.getDefault())) { String message = i18n(WrongType, validationUtil.getMsgAndArgs()); addError(ValidationError.newValidationError() .validationErrorType(WrongType) diff --git a/src/main/java/graphql/validation/rules/KnownArgumentNames.java b/src/main/java/graphql/validation/rules/KnownArgumentNames.java index df0348e900..8f123f7412 100644 --- a/src/main/java/graphql/validation/rules/KnownArgumentNames.java +++ b/src/main/java/graphql/validation/rules/KnownArgumentNames.java @@ -8,7 +8,6 @@ import graphql.validation.AbstractRule; import graphql.validation.ValidationContext; import graphql.validation.ValidationErrorCollector; -import graphql.validation.ValidationErrorType; import static graphql.validation.ValidationErrorType.UnknownArgument; import static graphql.validation.ValidationErrorType.UnknownDirective; diff --git a/src/main/java/graphql/validation/rules/NoUnusedVariables.java b/src/main/java/graphql/validation/rules/NoUnusedVariables.java index 165fa2092c..80e3aa9429 100644 --- a/src/main/java/graphql/validation/rules/NoUnusedVariables.java +++ b/src/main/java/graphql/validation/rules/NoUnusedVariables.java @@ -8,7 +8,6 @@ import graphql.validation.AbstractRule; import graphql.validation.ValidationContext; import graphql.validation.ValidationErrorCollector; -import graphql.validation.ValidationErrorType; import java.util.ArrayList; import java.util.LinkedHashSet; diff --git a/src/main/java/graphql/validation/rules/UniqueOperationNames.java b/src/main/java/graphql/validation/rules/UniqueOperationNames.java index 3b6d1e08f2..f3eecf00d8 100644 --- a/src/main/java/graphql/validation/rules/UniqueOperationNames.java +++ b/src/main/java/graphql/validation/rules/UniqueOperationNames.java @@ -13,7 +13,7 @@ /** * A GraphQL document is only valid if all defined operations have unique names. - * http://facebook.github.io/graphql/October2016/#sec-Operation-Name-Uniqueness + * https://spec.graphql.org/October2021/#sec-Operation-Name-Uniqueness */ @Internal public class UniqueOperationNames extends AbstractRule { diff --git a/src/main/java/graphql/validation/rules/VariableDefaultValuesOfCorrectType.java b/src/main/java/graphql/validation/rules/VariableDefaultValuesOfCorrectType.java index e00f1b87da..c67026e8b6 100644 --- a/src/main/java/graphql/validation/rules/VariableDefaultValuesOfCorrectType.java +++ b/src/main/java/graphql/validation/rules/VariableDefaultValuesOfCorrectType.java @@ -7,6 +7,8 @@ import graphql.validation.ValidationContext; import graphql.validation.ValidationErrorCollector; +import java.util.Locale; + import static graphql.schema.GraphQLTypeUtil.simplePrint; import static graphql.validation.ValidationErrorType.BadValueForDefaultArg; @@ -25,7 +27,7 @@ public void checkVariableDefinition(VariableDefinition variableDefinition) { return; } if (variableDefinition.getDefaultValue() != null - && !getValidationUtil().isValidLiteralValue(variableDefinition.getDefaultValue(), inputType, getValidationContext().getSchema())) { + && !getValidationUtil().isValidLiteralValue(variableDefinition.getDefaultValue(), inputType, getValidationContext().getSchema(), getValidationContext().getGraphQLContext(), Locale.getDefault())) { String message = i18n(BadValueForDefaultArg, "VariableDefaultValuesOfCorrectType.badDefault", variableDefinition.getDefaultValue(), simplePrint(inputType)); addError(BadValueForDefaultArg, variableDefinition.getSourceLocation(), message); } diff --git a/src/main/java/graphql/validation/rules/VariableTypesMatch.java b/src/main/java/graphql/validation/rules/VariableTypesMatch.java index 72ca603df2..de395850af 100644 --- a/src/main/java/graphql/validation/rules/VariableTypesMatch.java +++ b/src/main/java/graphql/validation/rules/VariableTypesMatch.java @@ -65,14 +65,14 @@ public void checkVariable(VariableReference variableReference) { if (schemaDefault.isPresent() && schemaDefault.get().isLiteral()) { schemaDefaultValue = (Value) schemaDefault.get().getValue(); } else if (schemaDefault.isPresent() && schemaDefault.get().isSet()) { - schemaDefaultValue = ValuesResolver.valueToLiteral(schemaDefault.get(), expectedType); + schemaDefaultValue = ValuesResolver.valueToLiteral(schemaDefault.get(), expectedType, getValidationContext().getGraphQLContext(), getValidationContext().getI18n().getLocale()); } if (expectedType == null) { // we must have a unknown variable say to not have a known type return; } if (!variablesTypesMatcher.doesVariableTypesMatch(variableType, variableDefinition.getDefaultValue(), expectedType) && - !variablesTypesMatcher.doesVariableTypesMatch(variableType, schemaDefaultValue, expectedType)) { + !variablesTypesMatcher.doesVariableTypesMatch(variableType, schemaDefaultValue, expectedType)) { GraphQLType effectiveType = variablesTypesMatcher.effectiveType(variableType, variableDefinition.getDefaultValue()); String message = i18n(VariableTypeMismatch, "VariableTypesMatchRule.unexpectedType", GraphQLTypeUtil.simplePrint(effectiveType), diff --git a/src/main/resources/i18n/Parsing.properties b/src/main/resources/i18n/Parsing.properties new file mode 100644 index 0000000000..1152e601e5 --- /dev/null +++ b/src/main/resources/i18n/Parsing.properties @@ -0,0 +1,26 @@ +# +# This resource bundle is used for the query parsing code to produce i18n messages +# +# The keys have the format of rule class name and then message type within that. Most rules +# will only have 1 or 2 message keys +# +# Please try and keep this sorted within rule class and use # between sections so the IDEA Ctrl-Alt-L reformat does not bunch +# them too tightly. +# +# REMEMBER - a single quote ' in MessageFormat means things that are never replaced within them +# so use 2 '' characters to make it one ' on output. This will take for the form ''{0}'' +# +InvalidSyntax.noMessage=Invalid syntax at line {0} column {1} +InvalidSyntax.full=Invalid syntax with ANTLR error ''{0}'' at line {1} column {2} + +InvalidSyntaxBail.noToken=Invalid syntax at line {0} column {1} +InvalidSyntaxBail.full=Invalid syntax with offending token ''{0}'' at line {1} column {2} +# +InvalidSyntaxMoreTokens.full=Invalid syntax encountered. There are extra tokens in the text that have not been consumed. Offending token ''{0}'' at line {1} column {2} +# +ParseCancelled.full=More than {0} ''{1}'' tokens have been presented. To prevent Denial Of Service attacks, parsing has been cancelled. +# +InvalidUnicode.trailingLeadingSurrogate=Invalid unicode encountered. Trailing surrogate must be preceded with a leading surrogate. Offending token ''{0}'' at line {1} column {2} +InvalidUnicode.leadingTrailingSurrogate=Invalid unicode encountered. Leading surrogate must be followed by a trailing surrogate. Offending token ''{0}'' at line {1} column {2} +InvalidUnicode.invalidCodePoint=Invalid unicode encountered. Not a valid code point. Offending token ''{0}'' at line {1} column {2} +InvalidUnicode.incorrectEscape=Invalid unicode encountered. Incorrectly formatted escape sequence. Offending token ''{0}'' at line {1} column {2} diff --git a/src/main/resources/i18n/Parsing_de.properties b/src/main/resources/i18n/Parsing_de.properties new file mode 100644 index 0000000000..9438eaf8aa --- /dev/null +++ b/src/main/resources/i18n/Parsing_de.properties @@ -0,0 +1,29 @@ +# +# This resource bundle is used for the query parsing code to produce i18n messages +# +# The keys have the format of rule class name and then message type within that. Most rules +# will only have 1 or 2 message keys +# +# Please try and keep this sorted within rule class and use # between sections so the IDEA Ctrl-Alt-L reformat does not bunch +# them too tightly. +# +# REMEMBER - a single quote ' in MessageFormat means things that are never replaced within them +# so use 2 '' characters to make it one ' on output. This will take for the form ''{0}'' +# +# Prior to Java 9, properties files are encoded in ISO-8859-1. +# We have to use \u00fc instead of the German ue character, \u00e4 for ae, \u00f6 for oe, \u00df for ss +# +InvalidSyntax.noMessage=Ung\u00fcltige Syntax in Zeile {0} Spalte {1} +InvalidSyntax.full=Ung\u00fcltige Syntax, ANTLR-Fehler ''{0}'' in Zeile {1} Spalte {2} + +InvalidSyntaxBail.noToken=Ung\u00fcltige Syntax in Zeile {0} Spalte {1} +InvalidSyntaxBail.full=Ung\u00fcltige Syntax wegen des ung\u00fcltigen Tokens ''{0}'' in Zeile {1} Spalte {2} +# +InvalidSyntaxMoreTokens.full=Es wurde eine ung\u00fcltige Syntax festgestellt. Es gibt zus\u00e4tzliche Token im Text, die nicht konsumiert wurden. Ung\u00fcltiges Token ''{0}'' in Zeile {1} Spalte {2} +# +ParseCancelled.full=Es wurden mehr als {0} ''{1}'' Token pr\u00e4sentiert. Um Denial-of-Service-Angriffe zu verhindern, wurde das Parsing abgebrochen +# +InvalidUnicode.trailingLeadingSurrogate=Ung\u00fcltiger Unicode gefunden. Trailing surrogate muss ein leading surrogate vorangestellt werden. Ung\u00fcltiges Token ''{0}'' in Zeile {1} Spalte {2} +InvalidUnicode.leadingTrailingSurrogate=Ung\u00fcltiger Unicode gefunden. Auf ein leading surrogate muss ein trailing surrogate folgen. Ung\u00fcltiges Token ''{0}'' in Zeile {1} Spalte {2} +InvalidUnicode.invalidCodePoint=Ung\u00fcltiger Unicode gefunden. Kein g\u00fcltiger code point. Ung\u00fcltiges Token ''{0}'' in Zeile {1} Spalte {2} +InvalidUnicode.incorrectEscape=Ung\u00fcltiger Unicode gefunden. Falsch formatierte Escape-Sequenz. Ung\u00fcltiges Token ''{0}'' in Zeile {1} Spalte {2} \ No newline at end of file diff --git a/src/main/resources/i18n/Scalars.properties b/src/main/resources/i18n/Scalars.properties new file mode 100644 index 0000000000..39fc6b4105 --- /dev/null +++ b/src/main/resources/i18n/Scalars.properties @@ -0,0 +1,33 @@ +# +# This resource bundle is used for the scalar code to produce i18n messages +# +# The keys have the format of rule class name and then message type within that. Most rules +# will only have 1 or 2 message keys +# +# Please try and keep this sorted within rule class and use # between sections so the IDEA Ctrl-Alt-L reformat does not bunch +# them too tightly. +# +# REMEMBER - a single quote ' in MessageFormat means things that are never replaced within them +# so use 2 '' characters to make it one ' on output. This will take for the form ''{0}'' +# +Scalar.unexpectedAstType=Expected an AST type of ''{0}'' but it was a ''{1}'' +# +Enum.badInput=Invalid input for enum ''{0}''. Unknown value ''{1}'' +Enum.badName=Invalid input for enum ''{0}''. No value found for name ''{1}'' +Enum.unallowableValue=Literal value not in allowable values for enum ''{0}'' - ''{1}'' +# +Int.notInt=Expected a value that can be converted to type ''Int'' but it was a ''{0}'' +Int.outsideRange=Expected value to be in the integer range, but it was a ''{0}'' +# +ID.notId=Expected a value that can be converted to type ''ID'' but it was a ''{0}'' +ID.unexpectedAstType=Expected an AST type of ''IntValue'' or ''StringValue'' but it was a ''{0}'' +# +Float.notFloat=Expected a value that can be converted to type ''Float'' but it was a ''{0}'' +Float.unexpectedAstType=Expected an AST type of ''IntValue'' or ''FloatValue'' but it was a ''{0}'' +Float.unexpectedRawValueType=Expected a Number input, but it was a ''{0}'' +# +Boolean.notBoolean=Expected a value that can be converted to type ''Boolean'' but it was a ''{0}'' +Boolean.unexpectedAstType=Expected an AST type of ''BooleanValue'' but it was a ''{0}'' +Boolean.unexpectedRawValueType=Expected a Boolean input, but it was a ''{0}'' +# +String.unexpectedRawValueType=Expected a String input, but it was a ''{0}'' diff --git a/src/main/resources/i18n/Scalars_de.properties b/src/main/resources/i18n/Scalars_de.properties new file mode 100644 index 0000000000..02b1b27a75 --- /dev/null +++ b/src/main/resources/i18n/Scalars_de.properties @@ -0,0 +1,36 @@ +# +# This resource bundle is used for the scalar code to produce i18n messages +# +# The keys have the format of rule class name and then message type within that. Most rules +# will only have 1 or 2 message keys +# +# Please try and keep this sorted within rule class and use # between sections so the IDEA Ctrl-Alt-L reformat does not bunch +# them too tightly. +# +# REMEMBER - a single quote ' in MessageFormat means things that are never replaced within them +# so use 2 '' characters to make it one ' on output. This will take for the form ''{0}'' +# +# Prior to Java 9, properties files are encoded in ISO-8859-1. +# We have to use \u00fc instead of the German ue character, \u00e4 for ae, \u00f6 for oe, \u00df for ss +# +Scalar.unexpectedAstType=Erwartet wurde ein AST type von ''{0}'', aber es war ein ''{1}'' +# +Enum.badInput=Ung\u00fcltige Eingabe f\u00fcr enum ''{0}''. Unbekannter Wert ''{1}'' +Enum.badName=Ung\u00fcltige Eingabe f\u00fcr enum ''{0}''. Kein Wert f\u00fcr den Namen ''{1}'' gefunden +Enum.unallowableValue=Literal nicht in den zul\u00e4ssigen Werten f\u00fcr enum ''{0}'' - ''{1}'' +# +Int.notInt=Erwartet wurde ein Wert, der in den Typ ''Int'' konvertiert werden kann, aber es war ein ''{0}'' +Int.outsideRange=Erwarteter Wert im Integer-Bereich, aber es war ein ''{0}'' +# +ID.notId=Erwartet wurde ein Wert, der in den Typ ''ID'' umgewandelt werden kann, aber es war ein ''{0}'' +ID.unexpectedAstType=Erwartet wurde ein AST type von ''IntValue'' oder ''StringValue'', aber es war ein ''{0}'' +# +Float.notFloat=Erwartet wurde ein Wert, der in den Typ ''Float'' konvertiert werden kann, aber es war ein ''{0}'' +Float.unexpectedAstType=Erwartet wurde ein AST type von ''IntValue'' oder ''FloatValue'', aber es war ein ''{0}'' +Float.unexpectedRawValueType=Erwartet wurde eine Number-Eingabe, aber es war ein ''{0}'' +# +Boolean.notBoolean=Erwartet wurde ein Wert, der in den Typ ''Boolean'' konvertiert werden kann, aber es war ein ''{0}'' +Boolean.unexpectedAstType=Erwartet wurde ein AST type ''BooleanValue'', aber es war ein ''{0}'' +Boolean.unexpectedRawValueType=Erwartet wurde eine Boolean-Eingabe, aber es war ein ''{0}'' +# +String.unexpectedRawValueType=Erwartet wurde eine String-Eingabe, aber es war ein ''{0}'' diff --git a/src/main/resources/i18n/Validation.properties b/src/main/resources/i18n/Validation.properties index c6eb1ae49b..3b2dfb731f 100644 --- a/src/main/resources/i18n/Validation.properties +++ b/src/main/resources/i18n/Validation.properties @@ -84,9 +84,11 @@ VariableTypesMatchRule.unexpectedType=Validation error ({0}) : Variable type ''{ ArgumentValidationUtil.handleNullError=Validation error ({0}) : argument ''{1}'' with value ''{2}'' must not be null # suppress inspection "UnusedProperty" ArgumentValidationUtil.handleScalarError=Validation error ({0}) : argument ''{1}'' with value ''{2}'' is not a valid ''{3}'' +# suppress inspection "UnusedProperty" ArgumentValidationUtil.handleScalarErrorCustomMessage=Validation error ({0}) : argument ''{1}'' with value ''{2}'' is not a valid ''{3}'' - {4} # suppress inspection "UnusedProperty" ArgumentValidationUtil.handleEnumError=Validation error ({0}) : argument ''{1}'' with value ''{2}'' is not a valid ''{3}'' +# suppress inspection "UnusedProperty" ArgumentValidationUtil.handleEnumErrorCustomMessage=Validation error ({0}) : argument ''{1}'' with value ''{2}'' is not a valid ''{3}'' - {4} # suppress inspection "UnusedProperty" ArgumentValidationUtil.handleNotObjectError=Validation error ({0}) : argument ''{1}'' with value ''{2}'' must be an object type diff --git a/src/main/resources/i18n/Validation_de.properties b/src/main/resources/i18n/Validation_de.properties new file mode 100644 index 0000000000..22059223e4 --- /dev/null +++ b/src/main/resources/i18n/Validation_de.properties @@ -0,0 +1,102 @@ +# +# This resource bundle is used for the query validation code to produce i18n messages +# +# The keys have the format of rule class name and then message type within that. Most rules +# will only have 1 or 2 message keys +# +# Please try and keep this sorted within rule class and use # between sections so the IDEA Ctrl-Alt-L reformat does not bunch +# them too tightly. +# +# REMEMBER - a single quote ' in MessageFormat means things that are never replaced within them +# so use 2 '' characters to make it one ' on output. This will take for the form ''{0}'' +# +# Prior to Java 9, properties files are encoded in ISO-8859-1. +# We have to use \u00fc instead of the German ue character, \u00e4 for ae, \u00f6 for oe, \u00df for ss +# +ExecutableDefinitions.notExecutableType=Validierungsfehler ({0}) : Type definition ''{1}'' ist nicht ausf\u00fchrbar +ExecutableDefinitions.notExecutableSchema=Validierungsfehler ({0}) : Schema definition ist nicht ausf\u00fchrbar +ExecutableDefinitions.notExecutableDirective=Validierungsfehler ({0}) : Directive definition ''{1}'' ist nicht ausf\u00fchrbar +ExecutableDefinitions.notExecutableDefinition=Validierungsfehler ({0}) : Die angegebene Definition ist nicht ausf\u00fchrbar +# +FieldsOnCorrectType.unknownField=Validierungsfehler ({0}) : Feld ''{1}'' vom Typ ''{2}'' ist nicht definiert +# +FragmentsOnCompositeType.invalidInlineTypeCondition=Validierungsfehler ({0}) : Inline fragment type condition ist ung\u00fcltig, muss auf Object/Interface/Union stehen +FragmentsOnCompositeType.invalidFragmentTypeCondition=Validierungsfehler ({0}) : Fragment type condition ist ung\u00fcltig, muss auf Object/Interface/Union stehen +# +KnownArgumentNames.unknownDirectiveArg=Validierungsfehler ({0}) : Unbekanntes directive argument ''{1}'' +KnownArgumentNames.unknownFieldArg=Validierungsfehler ({0}) : Unbekanntes field argument ''{1}'' +# +KnownDirectives.unknownDirective=Validierungsfehler ({0}) : Unbekannte directive ''{1}'' +KnownDirectives.directiveNotAllowed=Validierungsfehler ({0}) : Directive ''{1}'' ist hier nicht erlaubt +# +KnownFragmentNames.undefinedFragment=Validierungsfehler ({0}) : Undefiniertes Fragment ''{1}'' +# +KnownTypeNames.unknownType=Validierungsfehler ({0}) : Unbekannter Typ ''{1}'' +# +LoneAnonymousOperation.withOthers=Validierungsfehler ({0}) : Anonyme Operation mit anderen Operationen +LoneAnonymousOperation.namedOperation=Validierungsfehler ({0}) : Operation ''{1}'' folgt der anonymen Operation +# +NoFragmentCycles.cyclesNotAllowed=Validierungsfehler ({0}) : Fragment cycles nicht erlaubt +# +NoUndefinedVariables.undefinedVariable=Validierungsfehler ({0}) : Undefinierte Variable ''{1}'' +# +NoUnusedFragments.unusedFragments=Validierungsfehler ({0}) : Unbenutztes Fragment ''{1}'' +# +NoUnusedVariables.unusedVariable=Validierungsfehler ({0}) : Unbenutzte Variable ''{1}'' +# +OverlappingFieldsCanBeMerged.differentFields=Validierungsfehler ({0}) : ''{1}'' : ''{2}'' und ''{3}'' sind unterschiedliche Felder +OverlappingFieldsCanBeMerged.differentArgs=Validierungsfehler ({0}) : ''{1}'' : Felder haben unterschiedliche Argumente +OverlappingFieldsCanBeMerged.differentNullability=Validierungsfehler ({0}) : ''{1}'' : Felder haben unterschiedliche nullability shapes +OverlappingFieldsCanBeMerged.differentLists=Validierungsfehler ({0}) : ''{1}'' : Felder haben unterschiedliche list shapes +OverlappingFieldsCanBeMerged.differentReturnTypes=Validierungsfehler ({0}) : ''{1}'' : gibt verschiedene Typen ''{2}'' und ''{3}'' zur\u00fcck +# +PossibleFragmentSpreads.inlineIncompatibleTypes=Validierungsfehler ({0}) : Fragment kann hier nicht verbreitet werden, da object vom Typ ''{1}'' niemals vom Typ ''{2}'' sein k\u00f6nnen +PossibleFragmentSpreads.fragmentIncompatibleTypes=Validierungsfehler ({0}) : Fragment ''{1}'' kann hier nicht verbreitet werden, da object vom Typ ''{2}'' niemals vom Typ ''{3}'' sein k\u00f6nnen +# +ProvidedNonNullArguments.missingFieldArg=Validierungsfehler ({0}) : Fehlendes field argument ''{1}'' +ProvidedNonNullArguments.missingDirectiveArg=Validierungsfehler ({0}) : Fehlendes directive argument ''{1}'' +ProvidedNonNullArguments.nullValue=Validierungsfehler ({0}) : Nullwert f\u00fcr non-null field argument ''{1}'' +# +ScalarLeaves.subselectionOnLeaf=Validierungsfehler ({0}) : Unterauswahl f\u00fcr Blatttyp ''{1}'' von Feld ''{2}'' nicht zul\u00e4ssig +ScalarLeaves.subselectionRequired=Validierungsfehler ({0}) : Unterauswahl erforderlich f\u00fcr Typ ''{1}'' des Feldes ''{2}'' +# +SubscriptionUniqueRootField.multipleRootFields=Validierungsfehler ({0}) : Subscription operation ''{1}'' muss genau ein root field haben +SubscriptionUniqueRootField.multipleRootFieldsWithFragment=Validierungsfehler ({0}) : Subscription operation ''{1}'' muss genau ein root field mit Fragmenten haben +SubscriptionIntrospectionRootField.introspectionRootField=Validierungsfehler ({0}) : Subscription operation ''{1}'' root field ''{2}'' kann kein introspection field sein +SubscriptionIntrospectionRootField.introspectionRootFieldWithFragment=Validierungsfehler ({0}) : Subscription operation ''{1}'' fragment root field ''{2}'' kann kein introspection field sein +# +UniqueArgumentNames.uniqueArgument=Validierungsfehler ({0}) : Es kann nur ein Argument namens ''{1}'' geben +# +UniqueDirectiveNamesPerLocation.uniqueDirectives=Validierungsfehler ({0}) : Nicht wiederholbare directive m\u00fcssen innerhalb einer Lokation eindeutig benannt werden. Directive ''{1}'', die auf einem ''{2}'' verwendet wird, ist nicht eindeutig +# +UniqueFragmentNames.oneFragment=Validierungsfehler ({0}) : Es kann nur ein Fragment namens ''{1}'' geben +# +UniqueOperationNames.oneOperation=Validierungsfehler ({0}) : Es kann nur eine Operation namens ''{1}'' geben +# +UniqueVariableNames.oneVariable=Validierungsfehler ({0}) : Es kann nur eine Variable namens ''{1}'' geben +# +VariableDefaultValuesOfCorrectType.badDefault=Validierungsfehler ({0}) : Ung\u00fcltiger Standardwert ''{1}'' f\u00fcr Typ ''{2}'' +# +VariablesAreInputTypes.wrongType=Validierungsfehler ({0}) : Eingabevariable ''{1}'' Typ ''{2}'' ist kein Eingabetyp +# +VariableTypesMatchRule.unexpectedType=Validierungsfehler ({0}) : Der Variablentyp ''{1}'' stimmt nicht mit dem erwarteten Typ ''{2}'' \u00fcberein +# +# These are used but IDEA cant find them easily as being called +# +# suppress inspection "UnusedProperty" +ArgumentValidationUtil.handleNullError=Validierungsfehler ({0}) : Argument ''{1}'' mit Wert ''{2}'' darf nicht null sein +# suppress inspection "UnusedProperty" +ArgumentValidationUtil.handleScalarError=Validierungsfehler ({0}) : Argument ''{1}'' mit Wert ''{2}'' ist kein g\u00fcltiges ''{3}'' +# suppress inspection "UnusedProperty" +ArgumentValidationUtil.handleScalarErrorCustomMessage=Validierungsfehler ({0}) : Argument ''{1}'' mit Wert ''{2}'' ist kein g\u00fcltiges ''{3}'' - {4} +# suppress inspection "UnusedProperty" +ArgumentValidationUtil.handleEnumError=Validierungsfehler ({0}) : Argument ''{1}'' mit Wert ''{2}'' ist kein g\u00fcltiges ''{3}'' +# suppress inspection "UnusedProperty" +ArgumentValidationUtil.handleEnumErrorCustomMessage=Validierungsfehler ({0}) : Argument ''{1}'' mit Wert ''{2}'' ist kein g\u00fcltiges ''{3}'' - {4} +# suppress inspection "UnusedProperty" +ArgumentValidationUtil.handleNotObjectError=Validierungsfehler ({0}) : Argument ''{1}'' mit Wert ''{2}'' muss ein object type sein +# suppress inspection "UnusedProperty" +ArgumentValidationUtil.handleMissingFieldsError=Validierungsfehler ({0}) : Argument ''{1}'' mit Wert ''{2}'' fehlen Pflichtfelder ''{3}'' +# suppress inspection "UnusedProperty" +ArgumentValidationUtil.handleExtraFieldError=Validierungsfehler ({0}) : Argument ''{1}'' mit Wert ''{2}'' enth\u00e4lt ein Feld nicht in ''{3}'': ''{4}'' +# diff --git a/src/test/groovy/example/http/ExecutionResultJSONTesting.java b/src/test/groovy/example/http/ExecutionResultJSONTesting.java index 0ef1891d9a..74f5328a0d 100644 --- a/src/test/groovy/example/http/ExecutionResultJSONTesting.java +++ b/src/test/groovy/example/http/ExecutionResultJSONTesting.java @@ -70,7 +70,7 @@ private void testGson(HttpServletResponse response, Object er) throws IOExceptio private ExecutionResult createER() { List errors = new ArrayList<>(); - errors.add(new ValidationError(ValidationErrorType.UnknownType, mkLocations(), "Test ValidationError")); + errors.add(new ValidationError(ValidationErrorType.UnknownType, mkLocations(), "Test ValidationError")); // Retain as there is no alternative constructor for ValidationError errors.add(new MissingRootTypeException("Mutations are not supported.", null)); errors.add(new InvalidSyntaxError(mkLocations(), "Not good syntax m'kay")); errors.add(new NonNullableFieldWasNullError(new NonNullableFieldWasNullException(mkExecutionInfo(), mkPath()))); diff --git a/src/test/groovy/example/http/HttpMain.java b/src/test/groovy/example/http/HttpMain.java index 7fb2e4b08f..e705df61d6 100644 --- a/src/test/groovy/example/http/HttpMain.java +++ b/src/test/groovy/example/http/HttpMain.java @@ -18,6 +18,7 @@ import graphql.schema.idl.TypeDefinitionRegistry; import org.dataloader.BatchLoader; import org.dataloader.DataLoader; +import org.dataloader.DataLoaderFactory; import org.dataloader.DataLoaderRegistry; import org.eclipse.jetty.server.Handler; import org.eclipse.jetty.server.Request; @@ -46,7 +47,7 @@ import static java.util.Arrays.asList; /** - * An very simple example of serving a qraphql schema over http. + * A very simple example of serving a graphql schema over http. *

* More info can be found here : http://graphql.org/learn/serving-over-http/ */ @@ -178,7 +179,7 @@ private DataLoaderRegistry buildDataLoaderRegistry() { CompletableFuture.supplyAsync(() -> loadCharactersViaHTTP(keys)); - DataLoader friendsDataLoader = new DataLoader<>(friendsBatchLoader); + DataLoader friendsDataLoader = DataLoaderFactory.newDataLoader(friendsBatchLoader); DataLoaderRegistry dataLoaderRegistry = new DataLoaderRegistry(); // @@ -275,7 +276,7 @@ private Reader loadSchemaFile(String name) { } // Lots of the data happens to be maps of objects and this allows us to get back into type safety land - // with less boiler plate and casts + // with less boilerplate and casts // @SuppressWarnings("TypeParameterUnusedInFormals") private T asMapGet(Object mapObj, Object mapKey) { diff --git a/src/test/groovy/graphql/ContextPassingDataFetcher.groovy b/src/test/groovy/graphql/ContextPassingDataFetcher.groovy index 1f66d52ee6..4b667bf765 100644 --- a/src/test/groovy/graphql/ContextPassingDataFetcher.groovy +++ b/src/test/groovy/graphql/ContextPassingDataFetcher.groovy @@ -25,7 +25,7 @@ class ContextPassingDataFetcher implements DataFetcher { Integer localCtx = env.getLocalContext() if (localCtx == null) { - localCtx = env.getContext() + localCtx = env.getGraphQlContext().get("key") } def newData = data + localCtx + "," diff --git a/src/test/groovy/graphql/DataFetcherWithErrorsAndDataTest.groovy b/src/test/groovy/graphql/DataFetcherWithErrorsAndDataTest.groovy index 631b824838..2d5e8efa02 100644 --- a/src/test/groovy/graphql/DataFetcherWithErrorsAndDataTest.groovy +++ b/src/test/groovy/graphql/DataFetcherWithErrorsAndDataTest.groovy @@ -3,6 +3,10 @@ package graphql import graphql.execution.AsyncExecutionStrategy import graphql.execution.AsyncSerialExecutionStrategy import graphql.language.SourceLocation +import graphql.schema.DataFetcher +import graphql.schema.DataFetchingEnvironment +import graphql.schema.FieldCoordinates +import graphql.schema.GraphQLCodeRegistry import graphql.schema.GraphQLOutputType import graphql.schema.GraphQLSchema import graphql.schema.idl.RuntimeWiring @@ -32,55 +36,91 @@ class DataFetcherWithErrorsAndDataTest extends Specification { ChildObject child = new ChildObject() } - def executionInput(String query) { + static def executionInput(String query) { newExecutionInput().query(query).build() } + class ParentDataFetcher implements DataFetcher { + @Override + Object get(DataFetchingEnvironment environment) throws Exception { + return newResult() + .data(new ParentObject()) + .errors([newError() + .message("badField is bad") + .path(["root", "parent", "child", "badField"]) + .location(environment.getField().getSourceLocation()) + .build()]) + .build() + } + } + + class ChildDataFetcher implements DataFetcher { + @Override + Object get(DataFetchingEnvironment environment) throws Exception { + return newResult() + .data(["goodField": null, "badField": null]) + .errors([newError() + .message("goodField is bad") + .path(["root", "parent", "child", "goodField"]) + .location(environment.getField().getSourceLocation()) + .build(), + newError().message("badField is bad") + .path(["root", "parent", "child", "badField"]) + .location(environment.getField().getSourceLocation()) + .build()]) + .build() + } + } + @Unroll def "#820 - data fetcher can return data and errors (strategy: #strategyName)"() { - // see https://github.com/graphql-java/graphql-java/issues/820 given: - + def queryTypeName = "QueryType" + def rootFieldName = "root" + def rootTypeName = "rootType" + def parentFieldName = "parent" GraphQLOutputType childType = newObject() .name("childType") - .field(newFieldDefinition().name("goodField") + .field(newFieldDefinition() + .name("goodField") .type(GraphQLString)) - .field(newFieldDefinition().name("badField") + .field(newFieldDefinition() + .name("badField") .type(GraphQLString)) .build() GraphQLOutputType parentType = newObject() .name("parentType") - .field(newFieldDefinition().name("child") + .field(newFieldDefinition() + .name("child") .type(childType)) .build() GraphQLOutputType rootType = newObject() - .name("rootType") - .field(newFieldDefinition().name("parent") - .type(parentType) - .dataFetcher({ env -> - newResult() - .data(new ParentObject()) - .errors([newError() - .message("badField is bad") - .path(["root", "parent", "child", "badField"]) - .location(env.getField().getSourceLocation()) - .build()]) - .build() - - })) + .name(rootTypeName) + .field(newFieldDefinition() + .name(parentFieldName) + .type(parentType)) .build() - GraphQLSchema schema = newSchema().query( - newObject() - .name("QueryType") + def rootTypeCoordinates = FieldCoordinates.coordinates(queryTypeName, rootFieldName) + def parentTypeCoordinates = FieldCoordinates.coordinates(rootTypeName, parentFieldName) + DataFetcher rootTypeDataFetcher = { env -> [:] } + DataFetcher parentTypeDataFetcher = new ParentDataFetcher() + + GraphQLCodeRegistry codeRegistry = GraphQLCodeRegistry.newCodeRegistry() + .dataFetcher(rootTypeCoordinates, rootTypeDataFetcher) + .dataFetcher(parentTypeCoordinates, parentTypeDataFetcher) + .build() + + GraphQLSchema schema = newSchema() + .codeRegistry(codeRegistry) + .query(newObject() + .name(queryTypeName) .field(newFieldDefinition() - .name("root") + .name(rootFieldName) .type(rootType) - .dataFetcher({ env -> [:] }) - )) .build() @@ -122,52 +162,53 @@ class DataFetcherWithErrorsAndDataTest extends Specification { @Unroll def "#820 - data fetcher can return multiple errors (strategy: #strategyName)"() { - // see https://github.com/graphql-java/graphql-java/issues/820 given: - + def queryTypeName = "QueryType" + def rootFieldName = "root" + def parentTypeName = "parentType" + def childFieldName = "child" GraphQLOutputType childType = newObject() .name("childType") - .field(newFieldDefinition().name("goodField") + .field(newFieldDefinition() + .name("goodField") .type(GraphQLString)) - .field(newFieldDefinition().name("badField") + .field(newFieldDefinition() + .name("badField") .type(GraphQLString)) .build() GraphQLOutputType parentType = newObject() - .name("parentType") - .field(newFieldDefinition().name("child") - .type(childType) - .dataFetcher({ env -> - newResult() - .data(["goodField": null, "badField": null]) - .errors([ - newError().message("goodField is bad") - .path(["root", "parent", "child", "goodField"]) - .location(env.getField().getSourceLocation()) - .build(), - newError().message("badField is bad") - .path(["root", "parent", "child", "badField"]) - .location(env.getField().getSourceLocation()) - .build() - ]).build() - })) + .name(parentTypeName) + .field(newFieldDefinition() + .name(childFieldName) + .type(childType)) .build() GraphQLOutputType rootType = newObject() .name("rootType") - .field(newFieldDefinition().name("parent") + .field(newFieldDefinition() + .name("parent") .type(parentType)) .build() - GraphQLSchema schema = newSchema().query( - newObject() - .name("QueryType") + def rootTypeCoordinates = FieldCoordinates.coordinates(queryTypeName, rootFieldName) + def childTypeCoordinates = FieldCoordinates.coordinates(parentTypeName, childFieldName) + DataFetcher rootTypeDataFetcher = { env -> ["parent": [:]] } + DataFetcher childTypeDataFetcher = new ChildDataFetcher() + + GraphQLCodeRegistry codeRegistry = GraphQLCodeRegistry.newCodeRegistry() + .dataFetcher(rootTypeCoordinates, rootTypeDataFetcher) + .dataFetcher(childTypeCoordinates, childTypeDataFetcher) + .build() + + GraphQLSchema schema = newSchema() + .codeRegistry(codeRegistry) + .query(newObject() + .name(queryTypeName) .field(newFieldDefinition() - .name("root") + .name(rootFieldName) .type(rootType) - .dataFetcher({ env -> ["parent": [:]] }) - )) .build() @@ -210,7 +251,6 @@ class DataFetcherWithErrorsAndDataTest extends Specification { 'asyncSerial' | new AsyncSerialExecutionStrategy() } - @Unroll def "data fetcher can return context down each level (strategy: #strategyName)"() { given: @@ -278,7 +318,7 @@ class DataFetcherWithErrorsAndDataTest extends Specification { def result = TestUtil.graphQL(spec, runtimeWiring) .queryExecutionStrategy(executionStrategy) .build() - .execute(newExecutionInput().query(query).root("").context(1)) + .execute(newExecutionInput().query(query).root("").graphQLContext(["key": 1])) expect: diff --git a/src/test/groovy/graphql/ExecutionInputTest.groovy b/src/test/groovy/graphql/ExecutionInputTest.groovy index 2a55d073d1..f0b4b232fc 100644 --- a/src/test/groovy/graphql/ExecutionInputTest.groovy +++ b/src/test/groovy/graphql/ExecutionInputTest.groovy @@ -1,6 +1,5 @@ package graphql -import graphql.cachecontrol.CacheControl import graphql.execution.ExecutionId import graphql.schema.DataFetcher import graphql.schema.DataFetchingEnvironment @@ -13,31 +12,25 @@ class ExecutionInputTest extends Specification { def query = "query { hello }" def registry = new DataLoaderRegistry() - def cacheControl = CacheControl.newCacheControl() def root = "root" - def context = "context" def variables = [key: "value"] def "build works"() { when: def executionInput = ExecutionInput.newExecutionInput().query(query) .dataLoaderRegistry(registry) - .cacheControl(cacheControl) .variables(variables) .root(root) - .context(context) .graphQLContext({ it.of(["a": "b"]) }) .locale(Locale.GERMAN) .extensions([some: "map"]) .build() then: - executionInput.context == context executionInput.graphQLContext.get("a") == "b" executionInput.root == root executionInput.variables == variables executionInput.rawVariables.toMap() == variables executionInput.dataLoaderRegistry == registry - executionInput.cacheControl == cacheControl executionInput.query == query executionInput.locale == Locale.GERMAN executionInput.extensions == [some: "map"] @@ -53,28 +46,30 @@ class ExecutionInputTest extends Specification { } def "legacy context methods work"() { + // Retaining deprecated method tests for coverage when: def executionInput = ExecutionInput.newExecutionInput().query(query) - .context({ builder -> builder.of("k1", "v1") } as UnaryOperator) + .context({ builder -> builder.of("k1", "v1") } as UnaryOperator) // Retain deprecated for test coverage .build() then: - (executionInput.context as GraphQLContext).get("k1") == "v1" + (executionInput.context as GraphQLContext).get("k1") == "v1" // Retain deprecated for test coverage when: executionInput = ExecutionInput.newExecutionInput().query(query) - .context(GraphQLContext.newContext().of("k2", "v2")) + .context(GraphQLContext.newContext().of("k2", "v2")) // Retain deprecated for test coverage .build() then: - (executionInput.context as GraphQLContext).get("k2") == "v2" + (executionInput.context as GraphQLContext).get("k2") == "v2" // Retain deprecated for test coverage } def "legacy context is defaulted"() { + // Retaining deprecated method tests for coverage when: def executionInput = ExecutionInput.newExecutionInput().query(query) .build() then: - executionInput.context instanceof GraphQLContext - executionInput.getGraphQLContext() == executionInput.getContext() + executionInput.context instanceof GraphQLContext // Retain deprecated for test coverage + executionInput.getGraphQLContext() == executionInput.getContext() // Retain deprecated for test coverage } def "graphql context is defaulted"() { @@ -97,11 +92,9 @@ class ExecutionInputTest extends Specification { when: def executionInputOld = ExecutionInput.newExecutionInput().query(query) .dataLoaderRegistry(registry) - .cacheControl(cacheControl) .variables(variables) .extensions([some: "map"]) .root(root) - .context(context) .graphQLContext({ it.of(["a": "b"]) }) .locale(Locale.GERMAN) .build() @@ -109,12 +102,10 @@ class ExecutionInputTest extends Specification { def executionInput = executionInputOld.transform({ bldg -> bldg.query("new query") }) then: - executionInput.context == context executionInput.graphQLContext == graphQLContext executionInput.root == root executionInput.variables == variables executionInput.dataLoaderRegistry == registry - executionInput.cacheControl == cacheControl executionInput.locale == Locale.GERMAN executionInput.extensions == [some: "map"] executionInput.query == "new query" @@ -124,7 +115,6 @@ class ExecutionInputTest extends Specification { when: def executionInputOld = ExecutionInput.newExecutionInput().query(query) .dataLoaderRegistry(registry) - .cacheControl(cacheControl) .extensions([some: "map"]) .root(root) .graphQLContext({ it.of(["a": "b"]) }) @@ -140,7 +130,6 @@ class ExecutionInputTest extends Specification { executionInput.root == root executionInput.rawVariables.toMap() == variables executionInput.dataLoaderRegistry == registry - executionInput.cacheControl == cacheControl executionInput.locale == Locale.GERMAN executionInput.extensions == [some: "map"] executionInput.query == "new query" @@ -153,7 +142,6 @@ class ExecutionInputTest extends Specification { .build() then: executionInput.query == "{ q }" - executionInput.cacheControl != null executionInput.locale == Locale.ENGLISH executionInput.dataLoaderRegistry != null executionInput.variables == [:] @@ -169,7 +157,6 @@ class ExecutionInputTest extends Specification { DataFetcher df = { DataFetchingEnvironment env -> return [ "locale" : env.getLocale().getDisplayName(Locale.ENGLISH), - "cacheControl" : env.getCacheControl() == cacheControl, "executionId" : env.getExecutionId().toString(), "graphqlContext": env.getGraphQlContext().get("a") @@ -182,7 +169,6 @@ class ExecutionInputTest extends Specification { ExecutionInput executionInput = ExecutionInput.newExecutionInput() .query("{ fetch }") .locale(Locale.GERMAN) - .cacheControl(cacheControl) .executionId(ExecutionId.from("ID123")) .build() executionInput.getGraphQLContext().putAll([a: "b"]) @@ -191,6 +177,6 @@ class ExecutionInputTest extends Specification { then: er.errors.isEmpty() - er.data["fetch"] == "{locale=German, cacheControl=true, executionId=ID123, graphqlContext=b}" + er.data["fetch"] == "{locale=German, executionId=ID123, graphqlContext=b}" } } diff --git a/src/test/groovy/graphql/ExecutionResultTest.groovy b/src/test/groovy/graphql/ExecutionResultTest.groovy new file mode 100644 index 0000000000..67e411941c --- /dev/null +++ b/src/test/groovy/graphql/ExecutionResultTest.groovy @@ -0,0 +1,31 @@ +package graphql + +import graphql.language.SourceLocation +import spock.lang.Specification + +/** + * Most of the tests are actually in ExecutionResultImplTest since this is the actual impl + */ +class ExecutionResultTest extends Specification { + + def error1 = new InvalidSyntaxError(new SourceLocation(966, 964), "Yowza") + + def "can use builder to build it"() { + when: + ExecutionResult er = ExecutionResult.newExecutionResult().data([a: "b"]).addError(error1).addExtension("x", "y").build() + then: + er.data == [a: "b"] + er.errors == [error1] + er.extensions == [x: "y"] + } + + def "can transform"() { + when: + ExecutionResult er = ExecutionResult.newExecutionResult().data([a: "b"]).addError(error1).addExtension("x", "y").build() + er = er.transform({ bld -> bld.addExtension("foo", "bar") }) + then: + er.data == [a: "b"] + er.errors == [error1] + er.extensions == [x: "y", foo: "bar"] + } +} diff --git a/src/test/groovy/graphql/GarfieldSchema.java b/src/test/groovy/graphql/GarfieldSchema.java index 6c22d31ab4..1e82f7e091 100644 --- a/src/test/groovy/graphql/GarfieldSchema.java +++ b/src/test/groovy/graphql/GarfieldSchema.java @@ -1,6 +1,7 @@ package graphql; +import graphql.schema.GraphQLCodeRegistry; import graphql.schema.GraphQLInterfaceType; import graphql.schema.GraphQLObjectType; import graphql.schema.GraphQLSchema; @@ -111,23 +112,24 @@ public List getFriends() { .field(newFieldDefinition() .name("name") .type(GraphQLString)) - .typeResolver(new TypeResolver() { - @Override - public GraphQLObjectType getType(TypeResolutionEnvironment env) { - if (env.getObject() instanceof Dog) { - return DogType; - } - if (env.getObject() instanceof Person) { - return PersonType; - } - if (env.getObject() instanceof Cat) { - return CatType; - } - return null; - } - }) .build(); + public static TypeResolver namedTypeResolver = new TypeResolver() { + @Override + public GraphQLObjectType getType(TypeResolutionEnvironment env) { + if (env.getObject() instanceof Dog) { + return DogType; + } + if (env.getObject() instanceof Person) { + return PersonType; + } + if (env.getObject() instanceof Cat) { + return CatType; + } + return null; + } + }; + public static GraphQLObjectType DogType = newObject() .name("Dog") .field(newFieldDefinition() @@ -154,17 +156,18 @@ public GraphQLObjectType getType(TypeResolutionEnvironment env) { .name("Pet") .possibleType(CatType) .possibleType(DogType) - .typeResolver(env -> { - if (env.getObject() instanceof Cat) { - return CatType; - } - if (env.getObject() instanceof Dog) { - return DogType; - } - return null; - }) .build(); + public static TypeResolver petTypeResolver = env -> { + if (env.getObject() instanceof Cat) { + return CatType; + } + if (env.getObject() instanceof Dog) { + return DogType; + } + return null; + }; + public static GraphQLObjectType PersonType = newObject() .name("Person") .field(newFieldDefinition() @@ -179,9 +182,14 @@ public GraphQLObjectType getType(TypeResolutionEnvironment env) { .withInterface(NamedType) .build(); + public static GraphQLCodeRegistry codeRegistry = GraphQLCodeRegistry.newCodeRegistry() + .typeResolver("Named", namedTypeResolver) + .typeResolver("Pet", petTypeResolver) + .build(); + public static GraphQLSchema GarfieldSchema = GraphQLSchema.newSchema() .query(PersonType) + .codeRegistry(codeRegistry) .build(); - } diff --git a/src/test/groovy/graphql/GraphQLTest.groovy b/src/test/groovy/graphql/GraphQLTest.groovy index 0ae487f569..ec2523d3f8 100644 --- a/src/test/groovy/graphql/GraphQLTest.groovy +++ b/src/test/groovy/graphql/GraphQLTest.groovy @@ -2,7 +2,6 @@ package graphql import graphql.analysis.MaxQueryComplexityInstrumentation import graphql.analysis.MaxQueryDepthInstrumentation -import graphql.collect.ImmutableKit import graphql.execution.AsyncExecutionStrategy import graphql.execution.AsyncSerialExecutionStrategy import graphql.execution.DataFetcherExceptionHandler @@ -18,19 +17,21 @@ import graphql.execution.SubscriptionExecutionStrategy import graphql.execution.ValueUnboxer import graphql.execution.instrumentation.ChainedInstrumentation import graphql.execution.instrumentation.Instrumentation -import graphql.execution.instrumentation.SimpleInstrumentation +import graphql.execution.instrumentation.SimplePerformantInstrumentation import graphql.execution.instrumentation.dataloader.DataLoaderDispatcherInstrumentation import graphql.execution.preparsed.NoOpPreparsedDocumentProvider import graphql.language.SourceLocation import graphql.schema.DataFetcher import graphql.schema.DataFetchingEnvironment -import graphql.schema.GraphQLDirective +import graphql.schema.FieldCoordinates +import graphql.schema.GraphQLCodeRegistry import graphql.schema.GraphQLEnumType import graphql.schema.GraphQLFieldDefinition import graphql.schema.GraphQLInterfaceType import graphql.schema.GraphQLNonNull import graphql.schema.GraphQLObjectType import graphql.schema.GraphQLSchema +import graphql.schema.LightDataFetcher import graphql.schema.StaticDataFetcher import graphql.schema.idl.SchemaGenerator import graphql.schema.idl.errors.SchemaProblem @@ -41,6 +42,7 @@ import spock.lang.Specification import spock.lang.Unroll import java.util.concurrent.CompletableFuture +import java.util.function.Supplier import java.util.function.UnaryOperator import static graphql.ExecutionInput.Builder @@ -58,17 +60,23 @@ import static graphql.schema.GraphQLTypeReference.typeRef class GraphQLTest extends Specification { - GraphQLSchema simpleSchema() { + static GraphQLSchema simpleSchema() { GraphQLFieldDefinition.Builder fieldDefinition = newFieldDefinition() .name("hello") .type(GraphQLString) - .staticValue("world") - GraphQLSchema schema = newSchema().query( - newObject() + FieldCoordinates fieldCoordinates = FieldCoordinates.coordinates("RootQueryType", "hello") + DataFetcher dataFetcher = { env -> "world" } + GraphQLCodeRegistry codeRegistry = GraphQLCodeRegistry.newCodeRegistry() + .dataFetcher(fieldCoordinates, dataFetcher) + .build() + + GraphQLSchema schema = newSchema() + .codeRegistry(codeRegistry) + .query(newObject() .name("RootQueryType") .field(fieldDefinition) - .build() - ).build() + .build()) + .build() schema } @@ -81,7 +89,6 @@ class GraphQLTest extends Specification { then: result == [hello: 'world'] - } def "query with sub-fields"() { @@ -101,14 +108,20 @@ class GraphQLTest extends Specification { GraphQLFieldDefinition.Builder simpsonField = newFieldDefinition() .name("simpson") .type(heroType) - .staticValue([id: '123', name: 'homer']) - GraphQLSchema graphQLSchema = newSchema().query( - newObject() + FieldCoordinates fieldCoordinates = FieldCoordinates.coordinates("RootQueryType", "simpson") + DataFetcher dataFetcher = { env -> [id: '123', name: 'homer'] } + GraphQLCodeRegistry codeRegistry = GraphQLCodeRegistry.newCodeRegistry() + .dataFetcher(fieldCoordinates, dataFetcher) + .build() + + GraphQLSchema graphQLSchema = newSchema() + .codeRegistry(codeRegistry) + .query(newObject() .name("RootQueryType") .field(simpsonField) - .build() - ).build() + .build()) + .build() when: def result = GraphQL.newGraphQL(graphQLSchema).build().execute('{ simpson { id, name } }').data @@ -123,13 +136,20 @@ class GraphQLTest extends Specification { .name("hello") .type(GraphQLString) .argument(newArgument().name("arg").type(GraphQLString)) - .staticValue("world") - GraphQLSchema schema = newSchema().query( - newObject() + + FieldCoordinates fieldCoordinates = FieldCoordinates.coordinates("RootQueryType", "hello") + DataFetcher dataFetcher = { env -> "hello" } + GraphQLCodeRegistry codeRegistry = GraphQLCodeRegistry.newCodeRegistry() + .dataFetcher(fieldCoordinates, dataFetcher) + .build() + + GraphQLSchema schema = newSchema() + .codeRegistry(codeRegistry) + .query(newObject() .name("RootQueryType") .field(fieldDefinition) - .build() - ).build() + .build()) + .build() when: def errors = GraphQL.newGraphQL(schema).build().execute('{ hello(arg:11) }').errors @@ -205,7 +225,7 @@ class GraphQLTest extends Specification { then: errors.size() == 1 errors[0].errorType == ErrorType.InvalidSyntax - errors[0].message == "Invalid Syntax : Invalid unicode - leading surrogate must be followed by a trailing surrogate - offending token '\\ud83c' at line 1 column 13" + errors[0].message == "Invalid unicode encountered. Leading surrogate must be followed by a trailing surrogate. Offending token '\\ud83c' at line 1 column 13" errors[0].locations == [new SourceLocation(1, 13)] } @@ -239,14 +259,22 @@ class GraphQLTest extends Specification { set.add("One") set.add("Two") + def queryType = "QueryType" + def fieldName = "set" + def fieldCoordinates = FieldCoordinates.coordinates(queryType, fieldName) + DataFetcher dataFetcher = { set } + GraphQLCodeRegistry codeRegistry = GraphQLCodeRegistry.newCodeRegistry() + .dataFetcher(fieldCoordinates, dataFetcher) + .build() + def schema = newSchema() + .codeRegistry(codeRegistry) .query(newObject() - .name("QueryType") + .name(queryType) .field(newFieldDefinition() - .name("set") - .type(list(GraphQLString)) - .dataFetcher({ set }))) - .build() + .name(fieldName) + .type(list(GraphQLString))) + ).build() when: def data = GraphQL.newGraphQL(schema).build().execute("query { set }").data @@ -258,14 +286,30 @@ class GraphQLTest extends Specification { def "document with two operations executes specified operation"() { given: - GraphQLSchema schema = newSchema().query( - newObject() - .name("RootQueryType") - .field(newFieldDefinition().name("field1").type(GraphQLString).dataFetcher(new StaticDataFetcher("value1"))) - .field(newFieldDefinition().name("field2").type(GraphQLString).dataFetcher(new StaticDataFetcher("value2"))) - ) + def queryType = "RootQueryType" + def field1Name = "field1" + def field2Name = "field2" + def field1Coordinates = FieldCoordinates.coordinates(queryType, field1Name) + def field2Coordinates = FieldCoordinates.coordinates(queryType, field2Name) + DataFetcher field1DataFetcher = new StaticDataFetcher("value1") + DataFetcher field2DataFetcher = new StaticDataFetcher("value2") + GraphQLCodeRegistry codeRegistry = GraphQLCodeRegistry.newCodeRegistry() + .dataFetcher(field1Coordinates, field1DataFetcher) + .dataFetcher(field2Coordinates, field2DataFetcher) .build() + GraphQLSchema schema = newSchema() + .codeRegistry(codeRegistry) + .query(newObject() + .name(queryType) + .field(newFieldDefinition() + .name(field1Name) + .type(GraphQLString)) + .field(newFieldDefinition() + .name(field2Name) + .type(GraphQLString)) + ).build() + def query = """ query Query1 { field1 } query Query2 { field2 } @@ -352,240 +396,285 @@ class GraphQLTest extends Specification { def "query with int literal too large"() { given: - GraphQLSchema schema = newSchema().query( - newObject() - .name("QueryType") - .field( - newFieldDefinition() - .name("foo") - .type(GraphQLInt) - .argument(newArgument().name("bar").type(GraphQLInt).build()) - .dataFetcher({ return it.getArgument("bar") }) - )) + def queryType = "QueryType" + def fooName = "foo" + def fooCoordinates = FieldCoordinates.coordinates(queryType, fooName) + DataFetcher dataFetcher = { env -> env.getArgument("bar") } + GraphQLCodeRegistry codeRegistry = GraphQLCodeRegistry.newCodeRegistry() + .dataFetcher(fooCoordinates, dataFetcher) .build() + + GraphQLSchema schema = newSchema() + .codeRegistry(codeRegistry) + .query(newObject() + .name("QueryType") + .field(newFieldDefinition() + .name("foo") + .type(GraphQLInt) + .argument(newArgument().name("bar").type(GraphQLInt).build())) + ).build() def query = "{foo(bar: 12345678910)}" + when: def result = GraphQL.newGraphQL(schema).build().execute(query) then: result.errors.size() == 1 - result.errors[0].message == "Validation error (WrongType@[foo]) : argument 'bar' with value 'IntValue{value=12345678910}' is not a valid 'Int' - Expected value to be in the Integer range but it was '12345678910'" + result.errors[0].message == "Validation error (WrongType@[foo]) : argument 'bar' with value 'IntValue{value=12345678910}' is not a valid 'Int' - Expected value to be in the integer range, but it was a '12345678910'" } @SuppressWarnings("GroovyAssignabilityCheck") def "query with missing argument results in arguments map missing the key"() { given: - def dataFetcher = Mock(DataFetcher) - GraphQLSchema schema = newSchema().query( - newObject() - .name("QueryType") - .field( - newFieldDefinition() - .name("foo") - .type(GraphQLInt) - .argument(newArgument().name("bar").type(GraphQLInt).build()) - .dataFetcher(dataFetcher) - )) + def queryType = "QueryType" + def fooName = "foo" + def fooCoordinates = FieldCoordinates.coordinates(queryType, fooName) + def dataFetcher = Mock(LightDataFetcher) + GraphQLCodeRegistry codeRegistry = GraphQLCodeRegistry.newCodeRegistry() + .dataFetcher(fooCoordinates, dataFetcher) .build() + GraphQLSchema schema = newSchema() + .codeRegistry(codeRegistry) + .query(newObject() + .name(queryType) + .field(newFieldDefinition() + .name(fooName) + .type(GraphQLInt) + .argument(newArgument().name("bar").type(GraphQLInt).build())) + ).build() def query = "{foo}" + when: GraphQL.newGraphQL(schema).build().execute(query) then: - 1 * dataFetcher.get(_) >> { - DataFetchingEnvironment env -> - assert !env.arguments.containsKey('bar') + 1 * dataFetcher.get(_, _, _) >> { + def env = (it[2] as Supplier).get() + assert !env.arguments.containsKey('bar') } } @SuppressWarnings("GroovyAssignabilityCheck") def "query with null argument results in arguments map with value null "() { given: - def dataFetcher = Mock(DataFetcher) - GraphQLSchema schema = newSchema().query( - newObject() - .name("QueryType") - .field( - newFieldDefinition() - .name("foo") - .type(GraphQLInt) - .argument(newArgument().name("bar").type(GraphQLInt).build()) - .dataFetcher(dataFetcher) - )) + def queryType = "QueryType" + def fooName = "foo" + def fooCoordinates = FieldCoordinates.coordinates(queryType, fooName) + def dataFetcher = Mock(LightDataFetcher) + GraphQLCodeRegistry codeRegistry = GraphQLCodeRegistry.newCodeRegistry() + .dataFetcher(fooCoordinates, dataFetcher) .build() + GraphQLSchema schema = newSchema() + .codeRegistry(codeRegistry) + .query(newObject() + .name(queryType) + .field(newFieldDefinition() + .name(fooName) + .type(GraphQLInt) + .argument(newArgument().name("bar").type(GraphQLInt).build())) + ).build() def query = "{foo(bar: null)}" - DataFetchingEnvironment dataFetchingEnvironment + when: GraphQL.newGraphQL(schema).build().execute(query) then: - 1 * dataFetcher.get(_) >> { - DataFetchingEnvironment env -> - dataFetchingEnvironment = env - assert env.arguments.containsKey('bar') - assert env.arguments['bar'] == null + 1 * dataFetcher.get(_, _, _) >> { + def env = (it[2] as Supplier).get() + assert env.arguments.containsKey('bar') + assert env.arguments['bar'] == null } } @SuppressWarnings("GroovyAssignabilityCheck") def "query with missing key in an input object result in a map with missing key"() { given: - def dataFetcher = Mock(DataFetcher) def inputObject = newInputObject().name("bar") .field(newInputObjectField().name("someKey").type(GraphQLString).build()) .field(newInputObjectField().name("otherKey").type(GraphQLString).build()).build() - GraphQLSchema schema = newSchema().query( - newObject() - .name("QueryType") - .field( - newFieldDefinition() - .name("foo") - .type(GraphQLInt) - .argument(newArgument().name("bar").type(inputObject).build()) - .dataFetcher(dataFetcher) - )) + + def queryType = "QueryType" + def fooName = "foo" + def fooCoordinates = FieldCoordinates.coordinates(queryType, fooName) + def dataFetcher = Mock(LightDataFetcher) + GraphQLCodeRegistry codeRegistry = GraphQLCodeRegistry.newCodeRegistry() + .dataFetcher(fooCoordinates, dataFetcher) .build() + + GraphQLSchema schema = newSchema() + .codeRegistry(codeRegistry) + .query(newObject() + .name(queryType) + .field(newFieldDefinition() + .name(fooName) + .type(GraphQLInt) + .argument(newArgument().name("bar").type(inputObject).build())) + ).build() def query = "{foo(bar: {someKey: \"value\"})}" when: def result = GraphQL.newGraphQL(schema).build().execute(query) then: result.errors.size() == 0 - 1 * dataFetcher.get(_) >> { - DataFetchingEnvironment env -> - assert env.arguments.size() == 1 - assert env.arguments["bar"] instanceof Map - assert env.arguments['bar']['someKey'] == 'value' - assert !(env.arguments['bar'] as Map).containsKey('otherKey') + 1 * dataFetcher.get(_, _, _) >> { + def env = (it[2] as Supplier).get() + assert env.arguments.size() == 1 + assert env.arguments["bar"] instanceof Map + assert env.arguments['bar']['someKey'] == 'value' + assert !(env.arguments['bar'] as Map).containsKey('otherKey') } } @SuppressWarnings("GroovyAssignabilityCheck") def "query with null value in an input object result in a map with null as value"() { given: - def dataFetcher = Mock(DataFetcher) def inputObject = newInputObject().name("bar") .field(newInputObjectField().name("someKey").type(GraphQLString).build()) .field(newInputObjectField().name("otherKey").type(GraphQLString).build()).build() - GraphQLSchema schema = newSchema().query( - newObject() - .name("QueryType") - .field( - newFieldDefinition() - .name("foo") - .type(GraphQLInt) - .argument(newArgument().name("bar").type(inputObject).build()) - .dataFetcher(dataFetcher) - )) + + def queryType = "QueryType" + def fooName = "foo" + def fooCoordinates = FieldCoordinates.coordinates(queryType, fooName) + def dataFetcher = Mock(LightDataFetcher) + GraphQLCodeRegistry codeRegistry = GraphQLCodeRegistry.newCodeRegistry() + .dataFetcher(fooCoordinates, dataFetcher) .build() + GraphQLSchema schema = newSchema() + .codeRegistry(codeRegistry) + .query(newObject() + .name(queryType) + .field(newFieldDefinition() + .name(fooName) + .type(GraphQLInt) + .argument(newArgument().name("bar").type(inputObject).build())) + ).build() def query = "{foo(bar: {someKey: \"value\", otherKey: null})}" + when: def result = GraphQL.newGraphQL(schema).build().execute(query) then: result.errors.size() == 0 - 1 * dataFetcher.get(_) >> { - DataFetchingEnvironment env -> - assert env.arguments.size() == 1 - assert env.arguments["bar"] instanceof Map - assert env.arguments['bar']['someKey'] == 'value' - assert (env.arguments['bar'] as Map).containsKey('otherKey') - assert env.arguments['bar']['otherKey'] == null + 1 * dataFetcher.get(_, _, _) >> { + def env = (it[2] as Supplier).get() + assert env.arguments.size() == 1 + assert env.arguments["bar"] instanceof Map + assert env.arguments['bar']['someKey'] == 'value' + assert (env.arguments['bar'] as Map).containsKey('otherKey') + assert env.arguments['bar']['otherKey'] == null } } def "query with missing List input field results in a map with a missing key"() { given: - def dataFetcher = Mock(DataFetcher) def inputObject = newInputObject().name("bar") .field(newInputObjectField().name("list").type(list(GraphQLString)).build()) .build() - GraphQLSchema schema = newSchema().query( - newObject() - .name("QueryType") - .field( - newFieldDefinition() - .name("foo") - .type(GraphQLInt) - .argument(newArgument().name("bar").type(inputObject).build()) - .dataFetcher(dataFetcher) - )) + + def queryType = "QueryType" + def fooName = "foo" + def fooCoordinates = FieldCoordinates.coordinates(queryType, fooName) + def dataFetcher = Mock(LightDataFetcher) + GraphQLCodeRegistry codeRegistry = GraphQLCodeRegistry.newCodeRegistry() + .dataFetcher(fooCoordinates, dataFetcher) .build() + GraphQLSchema schema = newSchema() + .codeRegistry(codeRegistry) + .query(newObject() + .name(queryType) + .field(newFieldDefinition() + .name(fooName) + .type(GraphQLInt) + .argument(newArgument().name("bar").type(inputObject).build())) + ).build() def query = "{foo(bar: {})}" + when: def result = GraphQL.newGraphQL(schema).build().execute(query) then: result.errors.size() == 0 - 1 * dataFetcher.get(_) >> { - DataFetchingEnvironment env -> - assert env.arguments.size() == 1 - assert env.arguments["bar"] instanceof Map - assert !(env.arguments['bar'] as Map).containsKey('list') + 1 * dataFetcher.get(_, _, _) >> { + def env = (it[2] as Supplier).get() + assert env.arguments.size() == 1 + assert env.arguments["bar"] instanceof Map + assert !(env.arguments['bar'] as Map).containsKey('list') } } def "query with null List input field results in a map with null as key"() { given: - def dataFetcher = Mock(DataFetcher) def inputObject = newInputObject().name("bar") .field(newInputObjectField().name("list").type(list(GraphQLString)).build()) .build() - GraphQLSchema schema = newSchema().query( - newObject() - .name("QueryType") - .field( - newFieldDefinition() - .name("foo") - .type(GraphQLInt) - .argument(newArgument().name("bar").type(inputObject).build()) - .dataFetcher(dataFetcher) - )) + + def queryType = "QueryType" + def fooName = "foo" + def fooCoordinates = FieldCoordinates.coordinates(queryType, fooName) + def dataFetcher = Mock(LightDataFetcher) + GraphQLCodeRegistry codeRegistry = GraphQLCodeRegistry.newCodeRegistry() + .dataFetcher(fooCoordinates, dataFetcher) .build() + GraphQLSchema schema = newSchema() + .codeRegistry(codeRegistry) + .query(newObject() + .name(queryType) + .field(newFieldDefinition() + .name(fooName) + .type(GraphQLInt) + .argument(newArgument().name("bar").type(inputObject).build())) + ).build() def query = "{foo(bar: {list: null})}" + when: def result = GraphQL.newGraphQL(schema).build().execute(query) then: result.errors.size() == 0 - 1 * dataFetcher.get(_) >> { - DataFetchingEnvironment env -> - assert env.arguments.size() == 1 - assert env.arguments["bar"] instanceof Map - assert (env.arguments['bar'] as Map).containsKey('list') - assert env.arguments['bar']['list'] == null + 1 * dataFetcher.get(_, _, _) >> { + def env = (it[2] as Supplier).get() + assert env.arguments.size() == 1 + assert env.arguments["bar"] instanceof Map + assert (env.arguments['bar'] as Map).containsKey('list') + assert env.arguments['bar']['list'] == null } } def "query with List containing null input field results in a map with a list containing null"() { given: - def dataFetcher = Mock(DataFetcher) def inputObject = newInputObject().name("bar") .field(newInputObjectField().name("list").type(list(GraphQLString)).build()) .build() - GraphQLSchema schema = newSchema().query( - newObject() - .name("QueryType") - .field( - newFieldDefinition() - .name("foo") - .type(GraphQLInt) - .argument(newArgument().name("bar").type(inputObject).build()) - .dataFetcher(dataFetcher) - )) + + def queryType = "QueryType" + def fooName = "foo" + def fooCoordinates = FieldCoordinates.coordinates(queryType, fooName) + def dataFetcher = Mock(LightDataFetcher) + GraphQLCodeRegistry codeRegistry = GraphQLCodeRegistry.newCodeRegistry() + .dataFetcher(fooCoordinates, dataFetcher) .build() + GraphQLSchema schema = newSchema() + .codeRegistry(codeRegistry) + .query(newObject() + .name(queryType) + .field(newFieldDefinition() + .name(fooName) + .type(GraphQLInt) + .argument(newArgument().name("bar").type(inputObject).build())) + ).build() def query = "{foo(bar: {list: [null]})}" + when: def result = GraphQL.newGraphQL(schema).build().execute(query) then: result.errors.size() == 0 - 1 * dataFetcher.get(_) >> { - DataFetchingEnvironment env -> - assert env.arguments.size() == 1 - assert env.arguments["bar"] instanceof Map - assert (env.arguments['bar'] as Map).containsKey('list') - assert env.arguments['bar']['list'] == [null] + 1 * dataFetcher.get(_, _, _) >> { + def env = (it[2] as Supplier).get() + assert env.arguments.size() == 1 + assert env.arguments["bar"] instanceof Map + assert (env.arguments['bar'] as Map).containsKey('list') + assert env.arguments['bar']['list'] == [null] } } @@ -623,7 +712,6 @@ class GraphQLTest extends Specification { } - def "execution input passing builder"() { given: GraphQLSchema schema = simpleSchema() @@ -641,7 +729,6 @@ class GraphQLTest extends Specification { GraphQLSchema schema = simpleSchema() when: - def builderFunction = { it.query('{hello}') } as UnaryOperator def result = GraphQL.newGraphQL(schema).build().execute(builderFunction).data @@ -783,7 +870,6 @@ class GraphQLTest extends Specification { .name("id") .type(Scalars.GraphQLID) } as UnaryOperator) - .typeResolver({ type -> foo }) .build() GraphQLObjectType query = newObject() @@ -796,7 +882,12 @@ class GraphQLTest extends Specification { } as UnaryOperator) .build() + def codeRegistry = GraphQLCodeRegistry.newCodeRegistry() + .typeResolver(node, { type -> foo }) + .build() + GraphQLSchema schema = newSchema() + .codeRegistry(codeRegistry) .query(query) .build() @@ -835,7 +926,7 @@ class GraphQLTest extends Specification { def "graphql copying works as expected"() { - def instrumentation = new SimpleInstrumentation() + def instrumentation = new SimplePerformantInstrumentation() def hello = ExecutionId.from("hello") def executionIdProvider = new ExecutionIdProvider() { @Override @@ -865,7 +956,7 @@ class GraphQLTest extends Specification { when: // now make some changes - def newInstrumentation = new SimpleInstrumentation() + def newInstrumentation = new SimplePerformantInstrumentation() def goodbye = ExecutionId.from("goodbye") def newExecutionIdProvider = new ExecutionIdProvider() { @Override @@ -890,7 +981,7 @@ class GraphQLTest extends Specification { def "disabling data loader instrumentation leaves instrumentation as is"() { given: def queryStrategy = new CaptureStrategy() - def instrumentation = new SimpleInstrumentation() + def instrumentation = new SimplePerformantInstrumentation() def builder = GraphQL.newGraphQL(simpleSchema()) .queryExecutionStrategy(queryStrategy) .instrumentation(instrumentation) @@ -939,18 +1030,26 @@ class GraphQLTest extends Specification { def "query with triple quoted multi line strings"() { given: + def queryType = "Query" + def fieldName = "hello" + def fieldCoordinates = FieldCoordinates.coordinates(queryType, fieldName) + DataFetcher dataFetcher = { env -> env.getArgument("arg") } + GraphQLCodeRegistry codeRegistry = GraphQLCodeRegistry.newCodeRegistry() + .dataFetcher(fieldCoordinates, dataFetcher) + .build() + GraphQLFieldDefinition.Builder fieldDefinition = newFieldDefinition() - .name("hello") + .name(fieldName) .type(GraphQLString) .argument(newArgument().name("arg").type(GraphQLString)) - .dataFetcher({ env -> env.getArgument("arg") } - ) - GraphQLSchema schema = newSchema().query( - newObject() - .name("Query") + + GraphQLSchema schema = newSchema() + .codeRegistry(codeRegistry) + .query(newObject() + .name(queryType) .field(fieldDefinition) - .build() - ).build() + .build()) + .build() when: def result = GraphQL.newGraphQL(schema).build().execute('''{ hello(arg:""" @@ -1152,10 +1251,10 @@ many lines'''] GraphQLSchema schema = TestUtil.schema('type Query {foo: MyScalar} scalar MyScalar @specifiedBy(url:"myUrl")') when: - def result = GraphQL.newGraphQL(schema).build().execute('{__type(name: "MyScalar") {name specifiedByUrl}}').getData() + def result = GraphQL.newGraphQL(schema).build().execute('{__type(name: "MyScalar") {name specifiedByURL}}').getData() then: - result == [__type: [name: "MyScalar", specifiedByUrl: "myUrl"]] + result == [__type: [name: "MyScalar", specifiedByURL: "myUrl"]] } def "test DFR and CF"() { @@ -1316,30 +1415,8 @@ many lines'''] e.message.contains("an illegal value for the argument ") } - def "Applied schema directives arguments are validated for programmatic schemas"() { - given: - def arg = newArgument().name("arg").type(GraphQLInt).valueProgrammatic(ImmutableKit.emptyMap()).build() - def directive = GraphQLDirective.newDirective().name("cached").argument(arg).build() - def field = newFieldDefinition() - .name("hello") - .type(GraphQLString) - .argument(arg) - .withDirective(directive) - .build() - when: - newSchema().query( - newObject() - .name("Query") - .field(field) - .build()) - .build() - then: - def e = thrown(InvalidSchemaException) - e.message.contains("Invalid argument 'arg' for applied directive of name 'cached'") - } - def "getters work as expected"() { - Instrumentation instrumentation = new SimpleInstrumentation() + Instrumentation instrumentation = new SimplePerformantInstrumentation() when: def graphQL = GraphQL.newGraphQL(StarWarsSchema.starWarsSchema).instrumentation(instrumentation).build() then: @@ -1361,6 +1438,6 @@ many lines'''] when: def er = graphQL.execute(ei) then: - ! er.errors.isEmpty() + !er.errors.isEmpty() } } diff --git a/src/test/groovy/graphql/GraphqlErrorBuilderTest.groovy b/src/test/groovy/graphql/GraphqlErrorBuilderTest.groovy index 59eb7ba8d3..944e1fef35 100644 --- a/src/test/groovy/graphql/GraphqlErrorBuilderTest.groovy +++ b/src/test/groovy/graphql/GraphqlErrorBuilderTest.groovy @@ -137,4 +137,19 @@ class GraphqlErrorBuilderTest extends Specification { error.path == null error.extensions == null } + + def "can use a builder direct from graphql error"() { + when: + def error = GraphQLError.newError().message("msg") + .locations(null) + .extensions([x : "y"]) + .path(null) + .build() + then: + error.message == "msg" + error.locations == null + error.extensions == [x : "y"] + error.path == null + + } } \ No newline at end of file diff --git a/src/test/groovy/graphql/InterfacesImplementingInterfacesTest.groovy b/src/test/groovy/graphql/InterfacesImplementingInterfacesTest.groovy index 47af9e7155..b5813c4d0b 100644 --- a/src/test/groovy/graphql/InterfacesImplementingInterfacesTest.groovy +++ b/src/test/groovy/graphql/InterfacesImplementingInterfacesTest.groovy @@ -1,5 +1,6 @@ package graphql +import graphql.schema.GraphQLCodeRegistry import graphql.schema.GraphQLInterfaceType import graphql.schema.GraphQLObjectType import graphql.schema.GraphQLSchema @@ -970,14 +971,12 @@ class InterfacesImplementingInterfacesTest extends Specification { def node1Type = newInterface() .name("Node1") .field(newFieldDefinition().name("id1").type(GraphQLString).build()) - .typeResolver({ env -> Assert.assertShouldNeverHappen() }) - .build(); + .build() def node2Type = newInterface() .name("Node2") .field(newFieldDefinition().name("id2").type(GraphQLString).build()) - .typeResolver({ env -> Assert.assertShouldNeverHappen() }) - .build(); + .build() // references two interfaces: directly and via type ref def resource = newInterface() @@ -986,8 +985,7 @@ class InterfacesImplementingInterfacesTest extends Specification { .field(newFieldDefinition().name("id2").type(GraphQLString).build()) .withInterface(GraphQLTypeReference.typeRef("Node1")) .withInterface(node2Type) - .typeResolver({ env -> Assert.assertShouldNeverHappen() }) - .build(); + .build() def image = newObject() .name("Image") .field(newFieldDefinition().name("id1").type(GraphQLString).build()) @@ -1000,7 +998,17 @@ class InterfacesImplementingInterfacesTest extends Specification { .name("Query") .field(newFieldDefinition().name("image").type(image).build()) .build() - def schema = GraphQLSchema.newSchema().query(query).additionalType(node1Type).build(); + + def codeRegistry = GraphQLCodeRegistry.newCodeRegistry() + .typeResolver(node1Type, { env -> Assert.assertShouldNeverHappen() }) + .typeResolver(node2Type, { env -> Assert.assertShouldNeverHappen() }) + .typeResolver(resource, { env -> Assert.assertShouldNeverHappen() }) + .build() + def schema = GraphQLSchema.newSchema() + .codeRegistry(codeRegistry) + .query(query) + .additionalType(node1Type) + .build() when: def printedSchema = new SchemaPrinter().print(schema) @@ -1045,7 +1053,6 @@ type Query { .argument(newArgument().name("arg3").type(GraphQLString)) ) .field(newFieldDefinition().name("field4").type(GraphQLString)) - .typeResolver({ env -> Assert.assertShouldNeverHappen() }) .build() def interface2 = newInterface() @@ -1057,7 +1064,6 @@ type Query { .field(newFieldDefinition().name("field2").type(GraphQLInt)) .field(newFieldDefinition().name("field3").type(GraphQLString)) .withInterface(interface1) - .typeResolver({ env -> Assert.assertShouldNeverHappen() }) .build() def query = newObject() @@ -1065,9 +1071,16 @@ type Query { .field(newFieldDefinition().name("interface2").type(interface2).build()) .build() + def codeRegistry = GraphQLCodeRegistry.newCodeRegistry() + .typeResolver(interface1, { env -> Assert.assertShouldNeverHappen() }) + .typeResolver(interface2, { env -> Assert.assertShouldNeverHappen() }) + .build() when: - GraphQLSchema.newSchema().query(query).build() + GraphQLSchema.newSchema() + .codeRegistry(codeRegistry) + .query(query) + .build() then: def error = thrown(InvalidSchemaException) @@ -1094,7 +1107,6 @@ type Query { .argument(newArgument().name("arg3").type(GraphQLString)) ) .field(newFieldDefinition().name("field4").type(GraphQLString)) - .typeResolver({ env -> Assert.assertShouldNeverHappen() }) .build() def interface2 = newInterface() @@ -1110,7 +1122,6 @@ type Query { ) .field(newFieldDefinition().name("field4").type(GraphQLString)) .withInterface(interface1) - .typeResolver({ env -> Assert.assertShouldNeverHappen() }) .build() def query = newObject() @@ -1118,9 +1129,16 @@ type Query { .field(newFieldDefinition().name("interface2").type(interface2).build()) .build() + def codeRegistry = GraphQLCodeRegistry.newCodeRegistry() + .typeResolver(interface1, { env -> Assert.assertShouldNeverHappen() }) + .typeResolver(interface2, { env -> Assert.assertShouldNeverHappen() }) + .build() when: - GraphQLSchema.newSchema().query(query).build() + GraphQLSchema.newSchema() + .codeRegistry(codeRegistry) + .query(query) + .build() then: noExceptionThrown() @@ -1131,7 +1149,6 @@ type Query { def interface1 = newInterface() .name("Interface1") .field(newFieldDefinition().name("field1").type(GraphQLString)) - .typeResolver({ env -> Assert.assertShouldNeverHappen() }) .build() def interface2 = newInterface() @@ -1139,7 +1156,6 @@ type Query { .field(newFieldDefinition().name("field1").type(GraphQLString)) .field(newFieldDefinition().name("field2").type(GraphQLString)) .withInterface(interface1) - .typeResolver({ env -> Assert.assertShouldNeverHappen() }) .build() def type = newObject() @@ -1154,9 +1170,16 @@ type Query { .field(newFieldDefinition().name("find").type(type).build()) .build() + def codeRegistry = GraphQLCodeRegistry.newCodeRegistry() + .typeResolver(interface1, { env -> Assert.assertShouldNeverHappen() }) + .typeResolver(interface2, { env -> Assert.assertShouldNeverHappen() }) + .build() when: - GraphQLSchema.newSchema().query(query).build() + GraphQLSchema.newSchema() + .codeRegistry(codeRegistry) + .query(query) + .build() then: def error = thrown(InvalidSchemaException) @@ -1169,7 +1192,6 @@ type Query { def interface1 = newInterface() .name("Interface1") .field(newFieldDefinition().name("field1").type(GraphQLString)) - .typeResolver({ env -> Assert.assertShouldNeverHappen() }) .build() def interface2 = newInterface() @@ -1177,7 +1199,6 @@ type Query { .field(newFieldDefinition().name("field1").type(GraphQLString)) .field(newFieldDefinition().name("field2").type(GraphQLString)) .withInterface(interface1) - .typeResolver({ env -> Assert.assertShouldNeverHappen() }) .build() def type = newObject() @@ -1193,9 +1214,16 @@ type Query { .field(newFieldDefinition().name("find").type(type).build()) .build() + def codeRegistry = GraphQLCodeRegistry.newCodeRegistry() + .typeResolver(interface1, { env -> Assert.assertShouldNeverHappen() }) + .typeResolver(interface2, { env -> Assert.assertShouldNeverHappen() }) + .build() when: - GraphQLSchema.newSchema().query(query).build() + GraphQLSchema.newSchema() + .codeRegistry(codeRegistry) + .query(query) + .build() then: noExceptionThrown() @@ -1208,7 +1236,6 @@ type Query { .field(newFieldDefinition().name("field1").type(GraphQLString)) .withInterface(GraphQLTypeReference.typeRef("Interface3")) .withInterface(GraphQLTypeReference.typeRef("Interface2")) - .typeResolver({ env -> Assert.assertShouldNeverHappen() }) .build() def interface2 = newInterface() @@ -1216,7 +1243,6 @@ type Query { .field(newFieldDefinition().name("field1").type(GraphQLString)) .withInterface(interface1) .withInterface(GraphQLTypeReference.typeRef("Interface3")) - .typeResolver({ env -> Assert.assertShouldNeverHappen() }) .build() def interface3 = newInterface() @@ -1224,7 +1250,6 @@ type Query { .field(newFieldDefinition().name("field1").type(GraphQLString)) .withInterface(interface1) .withInterface(interface2) - .typeResolver({ env -> Assert.assertShouldNeverHappen() }) .build() def query = newObject() @@ -1232,9 +1257,17 @@ type Query { .field(newFieldDefinition().name("find").type(interface3).build()) .build() + def codeRegistry = GraphQLCodeRegistry.newCodeRegistry() + .typeResolver(interface1, { env -> Assert.assertShouldNeverHappen() }) + .typeResolver(interface2, { env -> Assert.assertShouldNeverHappen() }) + .typeResolver(interface3, { env -> Assert.assertShouldNeverHappen() }) + .build() when: - GraphQLSchema.newSchema().query(query).build() + GraphQLSchema.newSchema() + .codeRegistry(codeRegistry) + .query(query) + .build() then: def error = thrown(InvalidSchemaException) @@ -1305,14 +1338,14 @@ type Query { def typeDefinitionRegistry = new SchemaParser().parse(sdl) TypeResolver typeResolver = { env -> - Map obj = env.getObject(); - String id = (String) obj.get("id"); + Map obj = env.getObject() + String id = (String) obj.get("id") GraphQLSchema schema = env.getSchema() if (id == "1") { - return (GraphQLObjectType) schema.getType("Image"); + return (GraphQLObjectType) schema.getType("Image") } else { - return (GraphQLObjectType) schema.getType("File"); + return (GraphQLObjectType) schema.getType("File") } } diff --git a/src/test/groovy/graphql/Issue296.groovy b/src/test/groovy/graphql/Issue296.groovy deleted file mode 100644 index e0ccc13549..0000000000 --- a/src/test/groovy/graphql/Issue296.groovy +++ /dev/null @@ -1,84 +0,0 @@ -package graphql - -import spock.lang.Specification - -import static graphql.Scalars.GraphQLString -import static graphql.schema.GraphQLArgument.newArgument -import static graphql.schema.GraphQLFieldDefinition.newFieldDefinition -import static graphql.schema.GraphQLInputObjectField.newInputObjectField -import static graphql.schema.GraphQLInputObjectType.newInputObject -import static graphql.schema.GraphQLObjectType.newObject -import static graphql.schema.GraphQLSchema.newSchema - -class Issue296 extends Specification { - - def "test introspection for #296 with map"() { - - def graphql = GraphQL.newGraphQL(newSchema() - .query(newObject() - .name("Query") - .field(newFieldDefinition() - .name("field") - .type(GraphQLString) - .argument(newArgument() - .name("argument") - .type(newInputObject() - .name("InputObjectType") - .field(newInputObjectField() - .name("inputField") - .type(GraphQLString)) - .build()) - .defaultValue([inputField: 'value1'])))) - .build()) - .build() - - def query = '{ __type(name: "Query") { fields { args { defaultValue } } } }' - - expect: - // converts the default object value to AST, then graphql pretty prints that as the value - graphql.execute(query).data == - [__type: [fields: [[args: [[defaultValue: '{inputField : "value1"}']]]]]] - } - - class FooBar { - final String inputField = "foo" - final String bar = "bar" - - String getInputField() { - return inputField - } - - String getBar() { - return bar - } - } - - def "test introspection for #296 with some object"() { - - def graphql = GraphQL.newGraphQL(newSchema() - .query(newObject() - .name("Query") - .field(newFieldDefinition() - .name("field") - .type(GraphQLString) - .argument(newArgument() - .name("argument") - .type(newInputObject() - .name("InputObjectType") - .field(newInputObjectField() - .name("inputField") - .type(GraphQLString)) - .build()) - .defaultValue(new FooBar())))) - .build()) - .build() - - def query = '{ __type(name: "Query") { fields { args { defaultValue } } } }' - - expect: - // converts the default object value to AST, then graphql pretty prints that as the value - graphql.execute(query).data == - [__type: [fields: [[args: [[defaultValue: '{inputField : "foo"}']]]]]] - } -} - diff --git a/src/test/groovy/graphql/Issue739.groovy b/src/test/groovy/graphql/Issue739.groovy index 0ceb24dd91..e3e579ab6b 100644 --- a/src/test/groovy/graphql/Issue739.groovy +++ b/src/test/groovy/graphql/Issue739.groovy @@ -106,7 +106,7 @@ class Issue739 extends Specification { varResult.data == null varResult.errors.size() == 1 varResult.errors[0].errorType == ErrorType.ValidationError - varResult.errors[0].message == "Variable 'input' has an invalid value: Expected type 'Int' but was 'String'." + varResult.errors[0].message == "Variable 'input' has an invalid value: Expected a value that can be converted to type 'Int' but it was a 'String'" varResult.errors[0].locations == [new SourceLocation(1, 11)] } } diff --git a/src/test/groovy/graphql/MutationSchema.java b/src/test/groovy/graphql/MutationSchema.java index 2683652331..5956d7c56d 100644 --- a/src/test/groovy/graphql/MutationSchema.java +++ b/src/test/groovy/graphql/MutationSchema.java @@ -1,6 +1,9 @@ package graphql; +import graphql.schema.DataFetcher; +import graphql.schema.FieldCoordinates; +import graphql.schema.GraphQLCodeRegistry; import graphql.schema.GraphQLObjectType; import graphql.schema.GraphQLSchema; @@ -31,8 +34,8 @@ public void setTheNumber(int theNumber) { } public static class SubscriptionRoot { - List result = new ArrayList(); - List subscribers = new ArrayList(); + List result = new ArrayList<>(); + List subscribers = new ArrayList<>(); NumberHolder numberHolder; public SubscriptionRoot(int initalNumber) { @@ -89,23 +92,13 @@ public List getResult() { .type(numberHolderType) .argument(newArgument() .name("newNumber") - .type(GraphQLInt)) - .dataFetcher(environment -> { - Integer newNumber = environment.getArgument("newNumber"); - SubscriptionRoot root = environment.getSource(); - return root.changeNumber(newNumber); - })) + .type(GraphQLInt))) .field(newFieldDefinition() .name("failToChangeTheNumber") .type(numberHolderType) .argument(newArgument() .name("newNumber") - .type(GraphQLInt)) - .dataFetcher(environment -> { - Integer newNumber = environment.getArgument("newNumber"); - SubscriptionRoot root = environment.getSource(); - return root.failToChangeTheNumber(newNumber); - })) + .type(GraphQLInt))) .build(); public static GraphQLObjectType subscriptionType = GraphQLObjectType.newObject() @@ -115,16 +108,37 @@ public List getResult() { .type(numberHolderType) .argument(newArgument() .name("clientId") - .type(GraphQLInt)) - .dataFetcher(environment -> { - Integer clientId = environment.getArgument("clientId"); - SubscriptionRoot subscriptionRoot = environment.getSource(); - subscriptionRoot.subscribeToNumberChanges(clientId); - return subscriptionRoot.getNumberHolder(); - })) + .type(GraphQLInt))) + .build(); + + static FieldCoordinates changeMutationCoordinates = FieldCoordinates.coordinates("mutationType", "changeTheNumber"); + static DataFetcher changeMutationDataFetcher = environment -> { + Integer newNumber = environment.getArgument("newNumber"); + SubscriptionRoot root = environment.getSource(); + return root.changeNumber(newNumber); + }; + static FieldCoordinates failToChangeMutationCoordinates = FieldCoordinates.coordinates("mutationType", "failToChangeTheNumber"); + static DataFetcher failToChangeMutationDataFetcher = environment -> { + Integer newNumber = environment.getArgument("newNumber"); + SubscriptionRoot root = environment.getSource(); + return root.failToChangeTheNumber(newNumber); + }; + static FieldCoordinates changeNumberSubscribeCoordinates = FieldCoordinates.coordinates("subscriptionType", "changeNumberSubscribe"); + static DataFetcher changeNumberSubscribeDataFetcher = environment -> { + Integer clientId = environment.getArgument("clientId"); + SubscriptionRoot subscriptionRoot = environment.getSource(); + subscriptionRoot.subscribeToNumberChanges(clientId); + return subscriptionRoot.getNumberHolder(); + }; + + static GraphQLCodeRegistry codeRegistry = GraphQLCodeRegistry.newCodeRegistry() + .dataFetcher(changeMutationCoordinates, changeMutationDataFetcher) + .dataFetcher(failToChangeMutationCoordinates, failToChangeMutationDataFetcher) + .dataFetcher(changeNumberSubscribeCoordinates, changeNumberSubscribeDataFetcher) .build(); public static GraphQLSchema schema = newSchema() + .codeRegistry(codeRegistry) .query(queryType) .mutation(mutationType) .subscription(subscriptionType) diff --git a/src/test/groovy/graphql/MutationTest.groovy b/src/test/groovy/graphql/MutationTest.groovy index 68da613740..d99506880d 100644 --- a/src/test/groovy/graphql/MutationTest.groovy +++ b/src/test/groovy/graphql/MutationTest.groovy @@ -47,8 +47,8 @@ class MutationTest extends Specification { ] when: - def executionResult = GraphQL.newGraphQL(MutationSchema.schema).build().execute(query, new MutationSchema.SubscriptionRoot(6)) - + def ei = ExecutionInput.newExecutionInput(query).root(new MutationSchema.SubscriptionRoot(6)).build() + def executionResult = GraphQL.newGraphQL(MutationSchema.schema).build().execute(ei) then: executionResult.data == expectedResult @@ -93,8 +93,8 @@ class MutationTest extends Specification { ] when: - def executionResult = GraphQL.newGraphQL(MutationSchema.schema).build().execute(query, new MutationSchema.SubscriptionRoot(6)) - + def ei = ExecutionInput.newExecutionInput(query).root(new MutationSchema.SubscriptionRoot(6)).build() + def executionResult = GraphQL.newGraphQL(MutationSchema.schema).build().execute(ei) then: executionResult.data == expectedResult diff --git a/src/test/groovy/graphql/NestedInputSchema.java b/src/test/groovy/graphql/NestedInputSchema.java index add9b44c4d..6e19825914 100644 --- a/src/test/groovy/graphql/NestedInputSchema.java +++ b/src/test/groovy/graphql/NestedInputSchema.java @@ -1,6 +1,9 @@ package graphql; +import graphql.schema.DataFetcher; +import graphql.schema.FieldCoordinates; import graphql.schema.GraphQLArgument; +import graphql.schema.GraphQLCodeRegistry; import graphql.schema.GraphQLFieldDefinition; import graphql.schema.GraphQLInputObjectField; import graphql.schema.GraphQLInputObjectType; @@ -14,51 +17,53 @@ public class NestedInputSchema { - public static GraphQLSchema createSchema() { + GraphQLObjectType root = rootType(); + FieldCoordinates valueCoordinates = FieldCoordinates.coordinates("Root", "value"); + DataFetcher valueDataFetcher = environment -> { + Integer initialValue = environment.getArgument("initialValue"); + Map filter = environment.getArgument("filter"); + if (filter != null) { + if (filter.containsKey("even")) { + Boolean even = (Boolean) filter.get("even"); + if (even && (initialValue % 2 != 0)) { + return 0; + } else if (!even && (initialValue % 2 == 0)) { + return 0; + } + } + if (filter.containsKey("range")) { + Map range = (Map) filter.get("range"); + if (initialValue < range.get("lowerBound") || + initialValue > range.get("upperBound")) { + return 0; + } + } + } + return initialValue; + }; - GraphQLObjectType root = rootType(); + GraphQLCodeRegistry codeRegistry = GraphQLCodeRegistry.newCodeRegistry() + .dataFetcher(valueCoordinates, valueDataFetcher) + .build(); return GraphQLSchema.newSchema() + .codeRegistry(codeRegistry) .query(root) .build(); } - @SuppressWarnings("unchecked") public static GraphQLObjectType rootType() { return GraphQLObjectType.newObject() - .name("Root") .field(GraphQLFieldDefinition.newFieldDefinition() .name("value") .type(GraphQLInt) - .dataFetcher(environment -> { - Integer initialValue = environment.getArgument("initialValue"); - Map filter = environment.getArgument("filter"); - if (filter != null) { - if (filter.containsKey("even")) { - Boolean even = (Boolean) filter.get("even"); - if (even && (initialValue % 2 != 0)) { - return 0; - } else if (!even && (initialValue % 2 == 0)) { - return 0; - } - } - if (filter.containsKey("range")) { - Map range = (Map) filter.get("range"); - if (initialValue < range.get("lowerBound") || - initialValue > range.get("upperBound")) { - return 0; - } - } - } - return initialValue; - }) .argument(GraphQLArgument.newArgument() .name("intialValue") .type(GraphQLInt) - .defaultValue(5)) + .defaultValueProgrammatic(5)) .argument(GraphQLArgument.newArgument() .name("filter") .type(filterType()))) diff --git a/src/test/groovy/graphql/NonNullHandlingTest.groovy b/src/test/groovy/graphql/NonNullHandlingTest.groovy index b08b8759a6..a6a352ef93 100644 --- a/src/test/groovy/graphql/NonNullHandlingTest.groovy +++ b/src/test/groovy/graphql/NonNullHandlingTest.groovy @@ -2,6 +2,10 @@ package graphql import graphql.execution.AsyncExecutionStrategy import graphql.execution.AsyncSerialExecutionStrategy +import graphql.schema.DataFetcher +import graphql.schema.DataFetchingEnvironment +import graphql.schema.FieldCoordinates +import graphql.schema.GraphQLCodeRegistry import graphql.schema.GraphQLOutputType import graphql.schema.GraphQLSchema import spock.lang.Specification @@ -32,36 +36,47 @@ class NonNullHandlingTest extends Specification { SimpleObject nonNullParent = new SimpleObject() } - def executionInput(String query) { + static def executionInput(String query) { ExecutionInput.newExecutionInput().query(query).build() } @Unroll def "#268 - null child field values are allowed in nullable parent type (strategy: #strategyName)"() { - // see https://github.com/graphql-java/graphql-java/issues/268 given: - - + def rootTypeName = "RootQueryType" + def parentFieldName = "parent" GraphQLOutputType parentType = newObject() .name("currentType") - .field(newFieldDefinition().name("nullChild") + .field(newFieldDefinition() + .name("nullChild") .type(nonNull(GraphQLString))) - .field(newFieldDefinition().name("nonNullChild") + .field(newFieldDefinition() + .name("nonNullChild") .type(nonNull(GraphQLString))) .build() - GraphQLSchema schema = newSchema().query( - newObject() - .name("RootQueryType") + def parentCoordinates = FieldCoordinates.coordinates(rootTypeName, parentFieldName) + DataFetcher dataFetcher = new DataFetcher() { + @Override + Object get(DataFetchingEnvironment environment) throws Exception { + return new SimpleObject() + } + } + GraphQLCodeRegistry codeRegistry = GraphQLCodeRegistry.newCodeRegistry() + .dataFetcher(parentCoordinates, dataFetcher) + .build() + + GraphQLSchema schema = newSchema() + .codeRegistry(codeRegistry) + .query(newObject() + .name(rootTypeName) .field(newFieldDefinition() - .name("parent") + .name(parentFieldName) .type(parentType) // nullable parent - .dataFetcher({ env -> new SimpleObject() }) - - )) - .build() + ) + ).build() def query = """ query { @@ -92,11 +107,11 @@ class NonNullHandlingTest extends Specification { @Unroll def "#268 - null child field values are NOT allowed in non nullable parent types (strategy: #strategyName)"() { - // see https://github.com/graphql-java/graphql-java/issues/268 given: - + def rootTypeName = "RootQueryType" + def parentFieldName = "parent" GraphQLOutputType parentType = newObject() .name("currentType") @@ -106,17 +121,27 @@ class NonNullHandlingTest extends Specification { .type(nonNull(GraphQLString))) .build() - GraphQLSchema schema = newSchema().query( - newObject() - .name("RootQueryType") + def parentCoordinates = FieldCoordinates.coordinates(rootTypeName, parentFieldName) + DataFetcher dataFetcher = new DataFetcher() { + @Override + Object get(DataFetchingEnvironment environment) throws Exception { + return new SimpleObject() + } + } + GraphQLCodeRegistry codeRegistry = GraphQLCodeRegistry.newCodeRegistry() + .dataFetcher(parentCoordinates, dataFetcher) + .build() + + GraphQLSchema schema = newSchema() + .codeRegistry(codeRegistry) + .query(newObject() + .name(rootTypeName) .field( newFieldDefinition() - .name("parent") + .name(parentFieldName) .type(nonNull(parentType)) // non nullable parent - .dataFetcher({ env -> new SimpleObject() }) - - )) - .build() + ) + ).build() def query = """ query { @@ -148,38 +173,53 @@ class NonNullHandlingTest extends Specification { @Unroll def "#581 - null child field values are allowed in nullable grand parent type (strategy: #strategyName)"() { - given: + def rootTypeName = "RootQueryType" + def topFieldName = "top" GraphQLOutputType parentType = newObject() .name("parentType") - .field(newFieldDefinition().name("nullChild") + .field(newFieldDefinition() + .name("nullChild") .type(nonNull(GraphQLString))) - .field(newFieldDefinition().name("nonNullChild") + .field(newFieldDefinition() + .name("nonNullChild") .type(nonNull(GraphQLString))) .build() GraphQLOutputType topType = newObject() .name("topType") - .field(newFieldDefinition().name("nullParent") + .field(newFieldDefinition() + .name("nullParent") .type(nonNull(parentType))) - .field(newFieldDefinition().name("nonNullParent") + .field(newFieldDefinition() + .name("nonNullParent") .type(nonNull(parentType))) .build() - GraphQLSchema schema = newSchema().query( - newObject() - .name("RootQueryType") - .field( - newFieldDefinition() - .name("top") - .type(topType) // nullable grand parent - .dataFetcher({ env -> new ContainingObject() }) - - )) + def topCoordinates = FieldCoordinates.coordinates(rootTypeName, topFieldName) + DataFetcher dataFetcher = new DataFetcher() { + @Override + Object get(DataFetchingEnvironment environment) throws Exception { + return new ContainingObject() + } + } + GraphQLCodeRegistry codeRegistry = GraphQLCodeRegistry.newCodeRegistry() + .dataFetcher(topCoordinates, dataFetcher) .build() + GraphQLSchema schema = newSchema() + .codeRegistry(codeRegistry) + .query(newObject() + .name(rootTypeName) + .field(newFieldDefinition() + .name(topFieldName) + .type(topType) // nullable grand parent + + ) + ).build() + def query = """ query { top { @@ -215,36 +255,50 @@ class NonNullHandlingTest extends Specification { def "#581 - null child field values are NOT allowed in non nullable grand parent types (strategy: #strategyName)"() { given: - + def rootTypeName = "RootQueryType" + def topFieldName = "top" GraphQLOutputType parentType = newObject() .name("parentType") - .field(newFieldDefinition().name("nullChild") + .field(newFieldDefinition() + .name("nullChild") .type(nonNull(GraphQLString))) - .field(newFieldDefinition().name("nonNullChild") + .field(newFieldDefinition() + .name("nonNullChild") .type(nonNull(GraphQLString))) .build() GraphQLOutputType topType = newObject() .name("topType") - .field(newFieldDefinition().name("nullParent") + .field(newFieldDefinition() + .name("nullParent") .type(nonNull(parentType))) - .field(newFieldDefinition().name("nonNullParent") + .field(newFieldDefinition() + .name("nonNullParent") .type(nonNull(parentType))) .build() - GraphQLSchema schema = newSchema().query( - newObject() - .name("RootQueryType") - .field( - newFieldDefinition() - .name("top") - .type(nonNull(topType)) // non nullable grand parent - .dataFetcher({ env -> new ContainingObject() }) - - )) + def topCoordinates = FieldCoordinates.coordinates(rootTypeName, topFieldName) + DataFetcher dataFetcher = new DataFetcher() { + @Override + Object get(DataFetchingEnvironment environment) throws Exception { + return new ContainingObject() + } + } + GraphQLCodeRegistry codeRegistry = GraphQLCodeRegistry.newCodeRegistry() + .dataFetcher(topCoordinates, dataFetcher) .build() + GraphQLSchema schema = newSchema() + .codeRegistry(codeRegistry) + .query(newObject() + .name(rootTypeName) + .field(newFieldDefinition() + .name(topFieldName) + .type(nonNull(topType)) // non nullable grand parent + ) + ).build() + def query = """ query { top { @@ -280,34 +334,47 @@ class NonNullHandlingTest extends Specification { def "#561 - null entry in non null list type with non null wrapper list (strategy: #strategyName)"() { given: - + def rootTypeName = "RootQueryType" + def topFieldName = "top" GraphQLOutputType parentType = newObject() .name("parentType") - .field(newFieldDefinition().name("nonNullListWithNull") + .field(newFieldDefinition() + .name("nonNullListWithNull") .type(nonNull(list(nonNull(GraphQLString))))) .build() GraphQLOutputType topType = newObject() .name("topType") - .field(newFieldDefinition().name("nullParent") + .field(newFieldDefinition() + .name("nullParent") .type(nonNull(parentType))) - .field(newFieldDefinition().name("nonNullParent") + .field(newFieldDefinition() + .name("nonNullParent") .type(nonNull(parentType))) .build() - GraphQLSchema schema = newSchema().query( - newObject() - .name("RootQueryType") - .field( - newFieldDefinition() - .name("top") - .type(nonNull(topType)) // non nullable grand parent - .dataFetcher({ env -> new ContainingObject() }) - - )) + def topCoordinates = FieldCoordinates.coordinates(rootTypeName, topFieldName) + DataFetcher dataFetcher = new DataFetcher() { + @Override + Object get(DataFetchingEnvironment environment) throws Exception { + return new ContainingObject() + } + } + GraphQLCodeRegistry codeRegistry = GraphQLCodeRegistry.newCodeRegistry() + .dataFetcher(topCoordinates, dataFetcher) .build() + GraphQLSchema schema = newSchema() + .codeRegistry(codeRegistry) + .query(newObject() + .name(rootTypeName) + .field(newFieldDefinition() + .name(topFieldName) + .type(nonNull(topType)) // non nullable grand parent + ) + ).build() + def query = """ query { top { @@ -344,33 +411,47 @@ class NonNullHandlingTest extends Specification { given: + def rootTypeName = "RootQueryType" + def topFieldName = "top" GraphQLOutputType parentType = newObject() .name("parentType") - .field(newFieldDefinition().name("nullableListWithNull") + .field(newFieldDefinition() + .name("nullableListWithNull") .type(list(nonNull(GraphQLString)))) .build() GraphQLOutputType topType = newObject() .name("topType") - .field(newFieldDefinition().name("nullParent") + .field(newFieldDefinition() + .name("nullParent") .type(nonNull(parentType))) - .field(newFieldDefinition().name("nonNullParent") + .field(newFieldDefinition() + .name("nonNullParent") .type(nonNull(parentType))) .build() - GraphQLSchema schema = newSchema().query( - newObject() - .name("RootQueryType") - .field( - newFieldDefinition() - .name("top") - .type(nonNull(topType)) // non nullable grand parent - .dataFetcher({ env -> new ContainingObject() }) - - )) + def topCoordinates = FieldCoordinates.coordinates(rootTypeName, topFieldName) + DataFetcher dataFetcher = new DataFetcher() { + @Override + Object get(DataFetchingEnvironment environment) throws Exception { + return new ContainingObject() + } + } + GraphQLCodeRegistry codeRegistry = GraphQLCodeRegistry.newCodeRegistry() + .dataFetcher(topCoordinates, dataFetcher) .build() + GraphQLSchema schema = newSchema() + .codeRegistry(codeRegistry) + .query(newObject() + .name(rootTypeName) + .field(newFieldDefinition() + .name(topFieldName) + .type(nonNull(topType)) // non nullable grand parent + ) + ).build() + def query = """ query { top { diff --git a/src/test/groovy/graphql/NullValueSupportTest.groovy b/src/test/groovy/graphql/NullValueSupportTest.groovy index 633117438c..0a854cb6c3 100644 --- a/src/test/groovy/graphql/NullValueSupportTest.groovy +++ b/src/test/groovy/graphql/NullValueSupportTest.groovy @@ -10,7 +10,7 @@ import spock.lang.Unroll import static graphql.ExecutionInput.newExecutionInput /* - * Taken from http://facebook.github.io/graphql/#sec-Input-Objects + * Taken from https://spec.graphql.org/October2021/#sec-Input-Objects * * diff --git a/src/test/groovy/graphql/ParseAndValidateTest.groovy b/src/test/groovy/graphql/ParseAndValidateTest.groovy index 3b676bd8d4..949b4aeb5e 100644 --- a/src/test/groovy/graphql/ParseAndValidateTest.groovy +++ b/src/test/groovy/graphql/ParseAndValidateTest.groovy @@ -110,7 +110,7 @@ class ParseAndValidateTest extends Specification { result.variables == [var1: 1] result.syntaxException != null - (result.errors[0] as InvalidSyntaxError).message.contains("Invalid Syntax") + (result.errors[0] as InvalidSyntaxError).message.contains("Invalid syntax") } def "can use the graphql context to stop certain validation rules"() { diff --git a/src/test/groovy/graphql/RelaySchema.java b/src/test/groovy/graphql/RelaySchema.java index 751afa8ebe..52b5526277 100644 --- a/src/test/groovy/graphql/RelaySchema.java +++ b/src/test/groovy/graphql/RelaySchema.java @@ -1,6 +1,9 @@ package graphql; import graphql.relay.Relay; +import graphql.schema.DataFetcher; +import graphql.schema.FieldCoordinates; +import graphql.schema.GraphQLCodeRegistry; import graphql.schema.GraphQLInterfaceType; import graphql.schema.GraphQLNonNull; import graphql.schema.GraphQLObjectType; @@ -57,15 +60,18 @@ public class RelaySchema { .argument(newArgument() .name("id") .description("id of the thing") - .type(GraphQLNonNull.nonNull(GraphQLString))) - .dataFetcher(environment -> { - //TODO: implement - return null; - })) + .type(GraphQLNonNull.nonNull(GraphQLString)))) .build(); + public static FieldCoordinates thingCoordinates = FieldCoordinates.coordinates("RelayQuery", "thing"); + public static DataFetcher thingDataFetcher = environment -> null; + + public static GraphQLCodeRegistry codeRegistry = GraphQLCodeRegistry.newCodeRegistry() + .dataFetcher(thingCoordinates, thingDataFetcher) + .build(); public static GraphQLSchema Schema = GraphQLSchema.newSchema() + .codeRegistry(codeRegistry) .query(RelayQueryType) .additionalType(Relay.pageInfoType) .build(); diff --git a/src/test/groovy/graphql/ScalarsBooleanTest.groovy b/src/test/groovy/graphql/ScalarsBooleanTest.groovy index a3ce05f46c..5b316765ad 100644 --- a/src/test/groovy/graphql/ScalarsBooleanTest.groovy +++ b/src/test/groovy/graphql/ScalarsBooleanTest.groovy @@ -1,5 +1,6 @@ package graphql +import graphql.execution.CoercedVariables import graphql.language.BooleanValue import graphql.language.FloatValue import graphql.language.IntValue @@ -15,19 +16,29 @@ class ScalarsBooleanTest extends Specification { @Unroll def "Boolean parse literal #literal.value as #result"() { expect: - Scalars.GraphQLBoolean.getCoercing().parseLiteral(literal) == result + Scalars.GraphQLBoolean.getCoercing().parseLiteral(literal, CoercedVariables.emptyVariables(), GraphQLContext.default, Locale.default) == result where: literal | result new BooleanValue(true) | true new BooleanValue(false) | false + } + @Unroll + def "Boolean parse literal #literal.value as #result with deprecated method"() { + expect: + Scalars.GraphQLBoolean.getCoercing().parseLiteral(literal) == result // Retain deprecated method for test coverage + + where: + literal | result + new BooleanValue(true) | true + new BooleanValue(false) | false } @Unroll def "Boolean returns null for invalid #literal"() { when: - Scalars.GraphQLBoolean.getCoercing().parseLiteral(literal) + Scalars.GraphQLBoolean.getCoercing().parseLiteral(literal, CoercedVariables.emptyVariables(), GraphQLContext.default, Locale.default) then: thrown(CoercingParseLiteralException) @@ -41,8 +52,28 @@ class ScalarsBooleanTest extends Specification { @Unroll def "Boolean serialize #value into #result (#result.class)"() { expect: - Scalars.GraphQLBoolean.getCoercing().serialize(value) == result - Scalars.GraphQLBoolean.getCoercing().parseValue(value) == result + Scalars.GraphQLBoolean.getCoercing().serialize(value, GraphQLContext.default, Locale.default) == result + + where: + value | result + true | true + "false" | false + "true" | true + "True" | true + 0 | false + 1 | true + -1 | true + new Long(42345784398534785l) | true + new Double(42.3) | true + new Float(42.3) | true + Integer.MAX_VALUE + 1l | true + Integer.MIN_VALUE - 1l | true + } + + @Unroll + def "Boolean serialize #value into #result (#result.class) with deprecated methods"() { + expect: + Scalars.GraphQLBoolean.getCoercing().serialize(value) == result // Retain deprecated method for test coverage where: value | result @@ -63,7 +94,7 @@ class ScalarsBooleanTest extends Specification { @Unroll def "serialize throws exception for invalid input #value"() { when: - Scalars.GraphQLBoolean.getCoercing().serialize(value) + Scalars.GraphQLBoolean.getCoercing().serialize(value, GraphQLContext.default, Locale.default) then: thrown(CoercingSerializeException) @@ -78,16 +109,49 @@ class ScalarsBooleanTest extends Specification { "f" | _ } + @Unroll + def "Boolean parseValue #value into #result (#result.class)"() { + expect: + Scalars.GraphQLBoolean.getCoercing().parseValue(value, GraphQLContext.default, Locale.default) == result + + where: + value | result + true | true + false | false + } + + @Unroll + def "Boolean parseValue #value into #result (#result.class) with deprecated methods"() { + expect: + Scalars.GraphQLBoolean.getCoercing().parseValue(value) == result // Retain deprecated method for test coverage + + where: + value | result + true | true + false | false + } + @Unroll def "parseValue throws exception for invalid input #value"() { when: - Scalars.GraphQLBoolean.getCoercing().parseValue(value) + Scalars.GraphQLBoolean.getCoercing().parseValue(value, GraphQLContext.default, Locale.default) then: thrown(CoercingParseValueException) where: - value | _ - new Object() | _ + value | _ + new Object() | _ + "false" | _ + "true" | _ + "True" | _ + 0 | _ + 1 | _ + -1 | _ + new Long(42345784398534785l) | _ + new Double(42.3) | _ + new Float(42.3) | _ + Integer.MAX_VALUE + 1l | _ + Integer.MIN_VALUE - 1l | _ } } diff --git a/src/test/groovy/graphql/ScalarsFloatTest.groovy b/src/test/groovy/graphql/ScalarsFloatTest.groovy index 910b731bc9..6f6a195d65 100644 --- a/src/test/groovy/graphql/ScalarsFloatTest.groovy +++ b/src/test/groovy/graphql/ScalarsFloatTest.groovy @@ -1,5 +1,6 @@ package graphql +import graphql.execution.CoercedVariables import graphql.language.BooleanValue import graphql.language.FloatValue import graphql.language.IntValue @@ -17,7 +18,7 @@ class ScalarsFloatTest extends Specification { @Unroll def "Float parse literal #literal.value as #result"() { expect: - Scalars.GraphQLFloat.getCoercing().parseLiteral(literal) == result + Scalars.GraphQLFloat.getCoercing().parseLiteral(literal, CoercedVariables.emptyVariables(), GraphQLContext.default, Locale.default) == result where: literal | result @@ -25,13 +26,25 @@ class ScalarsFloatTest extends Specification { new FloatValue(Double.MAX_VALUE) | Double.MAX_VALUE new FloatValue(Double.MIN_VALUE) | Double.MIN_VALUE new IntValue(12345678910 as BigInteger) | 12345678910 + } + @Unroll + def "Float parse literal #literal.value as #result with deprecated method"() { + expect: + Scalars.GraphQLFloat.getCoercing().parseLiteral(literal) == result // Retain deprecated for test coverage + + where: + literal | result + new FloatValue(42.442 as BigDecimal) | 42.442 + new FloatValue(Double.MAX_VALUE) | Double.MAX_VALUE + new FloatValue(Double.MIN_VALUE) | Double.MIN_VALUE + new IntValue(12345678910 as BigInteger) | 12345678910 } @Unroll def "Float returns null for invalid #literal"() { when: - Scalars.GraphQLFloat.getCoercing().parseLiteral(literal) + Scalars.GraphQLFloat.getCoercing().parseLiteral(literal, CoercedVariables.emptyVariables(), GraphQLContext.default, Locale.default) then: thrown(CoercingParseLiteralException) @@ -44,8 +57,32 @@ class ScalarsFloatTest extends Specification { @Unroll def "Float serialize #value into #result (#result.class)"() { expect: - Scalars.GraphQLFloat.getCoercing().serialize(value) == result - Scalars.GraphQLFloat.getCoercing().parseValue(value) == result + Scalars.GraphQLFloat.getCoercing().serialize(value, GraphQLContext.default, Locale.default) == result + + where: + value | result + "42" | 42d + "42.123" | 42.123d + 42.0000d | 42 + new Integer(42) | 42 + "-1" | -1 + new BigInteger("42") | 42 + new BigDecimal("42") | 42 + new BigDecimal("4.2") | 4.2d + 42.3f | 42.3d + 42.0d | 42d + new Byte("42") | 42 + new Short("42") | 42 + 1234567l | 1234567d + new AtomicInteger(42) | 42 + Double.MAX_VALUE | Double.MAX_VALUE + Double.MIN_VALUE | Double.MIN_VALUE + } + + @Unroll + def "Float serialize #value into #result (#result.class) with deprecated methods"() { + expect: + Scalars.GraphQLFloat.getCoercing().serialize(value) == result // Retain deprecated method for coverage where: value | result @@ -70,29 +107,99 @@ class ScalarsFloatTest extends Specification { @Unroll def "serialize throws exception for invalid input #value"() { when: - Scalars.GraphQLFloat.getCoercing().serialize(value) + Scalars.GraphQLFloat.getCoercing().serialize(value, GraphQLContext.default, Locale.default) then: thrown(CoercingSerializeException) where: - value | _ - "" | _ - "not a number " | _ - Double.NaN | _ + value | _ + "" | _ + "not a number " | _ + Double.NaN | _ + Double.NaN.toString() | _ + Double.POSITIVE_INFINITY | _ + Double.POSITIVE_INFINITY.toString() | _ + Double.NEGATIVE_INFINITY | _ + Double.NEGATIVE_INFINITY.toString() | _ + Float.NaN | _ + Float.NaN.toString() | _ + Float.POSITIVE_INFINITY | _ + Float.POSITIVE_INFINITY.toString() | _ + Float.NEGATIVE_INFINITY | _ + Float.NEGATIVE_INFINITY.toString() | _ + } + + @Unroll + def "Float parseValue #value into #result (#result.class)"() { + expect: + Scalars.GraphQLFloat.getCoercing().parseValue(value, GraphQLContext.default, Locale.default) == result + + where: + value | result + 42.0000d | 42 + new Integer(42) | 42 + new BigInteger("42") | 42 + new BigDecimal("42") | 42 + new BigDecimal("4.2") | 4.2d + 42.3f | 42.3d + 42.0d | 42d + new Byte("42") | 42 + new Short("42") | 42 + 1234567l | 1234567d + new AtomicInteger(42) | 42 + Double.MAX_VALUE | Double.MAX_VALUE + Double.MIN_VALUE | Double.MIN_VALUE } @Unroll - def "serialize/parseValue throws exception for invalid input #value"() { + def "Float parseValue #value into #result (#result.class) with deprecated methods"() { + expect: + Scalars.GraphQLFloat.getCoercing().parseValue(value) == result // Retain deprecated method for coverage + + where: + value | result + 42.0000d | 42 + new Integer(42) | 42 + new BigInteger("42") | 42 + new BigDecimal("42") | 42 + new BigDecimal("4.2") | 4.2d + 42.3f | 42.3d + 42.0d | 42d + new Byte("42") | 42 + new Short("42") | 42 + 1234567l | 1234567d + new AtomicInteger(42) | 42 + Double.MAX_VALUE | Double.MAX_VALUE + Double.MIN_VALUE | Double.MIN_VALUE + } + + + @Unroll + def "parseValue throws exception for invalid input #value"() { when: - Scalars.GraphQLFloat.getCoercing().parseValue(value) + Scalars.GraphQLFloat.getCoercing().parseValue(value, GraphQLContext.default, Locale.default) then: thrown(CoercingParseValueException) where: - value | _ - "" | _ - "not a number " | _ - Double.NaN | _ + value | _ + "" | _ + "not a number " | _ + Double.NaN | _ + Double.NaN.toString() | _ + Double.POSITIVE_INFINITY | _ + Double.POSITIVE_INFINITY.toString() | _ + Double.NEGATIVE_INFINITY | _ + Double.NEGATIVE_INFINITY.toString() | _ + Float.NaN | _ + Float.NaN.toString() | _ + Float.POSITIVE_INFINITY | _ + Float.POSITIVE_INFINITY.toString() | _ + Float.NEGATIVE_INFINITY | _ + Float.NEGATIVE_INFINITY.toString() | _ + "42" | _ + "42.123" | _ + "-1" | _ } } diff --git a/src/test/groovy/graphql/ScalarsIDTest.groovy b/src/test/groovy/graphql/ScalarsIDTest.groovy index 8a9e21362f..a304251796 100644 --- a/src/test/groovy/graphql/ScalarsIDTest.groovy +++ b/src/test/groovy/graphql/ScalarsIDTest.groovy @@ -1,5 +1,6 @@ package graphql +import graphql.execution.CoercedVariables import graphql.language.BooleanValue import graphql.language.IntValue import graphql.language.StringValue @@ -14,19 +15,29 @@ class ScalarsIDTest extends Specification { @Unroll def "ID parse literal #literal.value as #result"() { expect: - Scalars.GraphQLID.getCoercing().parseLiteral(literal) == result + Scalars.GraphQLID.getCoercing().parseLiteral(literal, CoercedVariables.emptyVariables(), GraphQLContext.default, Locale.default) == result where: literal | result new StringValue("1234ab") | "1234ab" new IntValue(123 as BigInteger) | "123" + } + + @Unroll + def "ID parse literal #literal.value as #result with deprecated method"() { + expect: + Scalars.GraphQLID.getCoercing().parseLiteral(literal) == result // Retain deprecated method for test coverage + where: + literal | result + new StringValue("1234ab") | "1234ab" + new IntValue(123 as BigInteger) | "123" } @Unroll def "ID returns null for invalid #literal"() { when: - Scalars.GraphQLID.getCoercing().parseLiteral(literal) + Scalars.GraphQLID.getCoercing().parseLiteral(literal, CoercedVariables.emptyVariables(), GraphQLContext.default, Locale.default) then: thrown(CoercingParseLiteralException) @@ -36,10 +47,10 @@ class ScalarsIDTest extends Specification { } @Unroll - def "ID serialize #value into #result (#result.class)"() { + def "ID serialize #value into #result (#result.class) with deprecated methods"() { expect: - Scalars.GraphQLID.getCoercing().serialize(value) == result - Scalars.GraphQLID.getCoercing().parseValue(value) == result + Scalars.GraphQLID.getCoercing().serialize(value) == result // Retain deprecated method for test coverage + Scalars.GraphQLID.getCoercing().parseValue(value) == result // Retain deprecated method for test coverage where: value | result diff --git a/src/test/groovy/graphql/ScalarsIntTest.groovy b/src/test/groovy/graphql/ScalarsIntTest.groovy index 2768b23654..7a5b43ed9e 100644 --- a/src/test/groovy/graphql/ScalarsIntTest.groovy +++ b/src/test/groovy/graphql/ScalarsIntTest.groovy @@ -1,5 +1,6 @@ package graphql +import graphql.execution.CoercedVariables import graphql.language.FloatValue import graphql.language.IntValue import graphql.language.StringValue @@ -16,20 +17,31 @@ class ScalarsIntTest extends Specification { @Unroll def "Int parse literal #literal.value as #result"() { expect: - Scalars.GraphQLInt.getCoercing().parseLiteral(literal) == result + Scalars.GraphQLInt.getCoercing().parseLiteral(literal, CoercedVariables.emptyVariables(), GraphQLContext.default, Locale.default) == result where: literal | result new IntValue(42 as BigInteger) | 42 new IntValue(Integer.MAX_VALUE as BigInteger) | Integer.MAX_VALUE new IntValue(Integer.MIN_VALUE as BigInteger) | Integer.MIN_VALUE + } + @Unroll + def "Int parse literal #literal.value as #result with deprecated method"() { + expect: + Scalars.GraphQLInt.getCoercing().parseLiteral(literal) == result // Retain deprecated method for test coverage + + where: + literal | result + new IntValue(42 as BigInteger) | 42 + new IntValue(Integer.MAX_VALUE as BigInteger) | Integer.MAX_VALUE + new IntValue(Integer.MIN_VALUE as BigInteger) | Integer.MIN_VALUE } @Unroll def "Int returns null for invalid #literal"() { when: - Scalars.GraphQLInt.getCoercing().parseLiteral(literal) + Scalars.GraphQLInt.getCoercing().parseLiteral(literal, CoercedVariables.emptyVariables(), GraphQLContext.default, Locale.default) then: thrown(CoercingParseLiteralException) @@ -42,14 +54,36 @@ class ScalarsIntTest extends Specification { new IntValue(Integer.MIN_VALUE - 1l as BigInteger) | _ new StringValue("-1") | _ new FloatValue(42.3) | _ - } @Unroll def "Int serialize #value into #result (#result.class)"() { expect: - Scalars.GraphQLInt.getCoercing().serialize(value) == result - Scalars.GraphQLInt.getCoercing().parseValue(value) == result + Scalars.GraphQLInt.getCoercing().serialize(value, GraphQLContext.default, Locale.default) == result + + where: + value | result + "42" | 42 + "42.0000" | 42 + 42.0000d | 42 + new Integer(42) | 42 + "-1" | -1 + new BigInteger("42") | 42 + new BigDecimal("42") | 42 + 42.0f | 42 + 42.0d | 42 + new Byte("42") | 42 + new Short("42") | 42 + 1234567l | 1234567 + new AtomicInteger(42) | 42 + Integer.MAX_VALUE | Integer.MAX_VALUE + Integer.MIN_VALUE | Integer.MIN_VALUE + } + + @Unroll + def "Int serialize #value into #result (#result.class) with deprecated methods"() { + expect: + Scalars.GraphQLInt.getCoercing().serialize(value) == result // Retain deprecated for test coverage where: value | result @@ -73,7 +107,7 @@ class ScalarsIntTest extends Specification { @Unroll def "serialize throws exception for invalid input #value"() { when: - Scalars.GraphQLInt.getCoercing().serialize(value) + Scalars.GraphQLInt.getCoercing().serialize(value, GraphQLContext.default, Locale.default) then: thrown(CoercingSerializeException) @@ -88,17 +122,57 @@ class ScalarsIntTest extends Specification { Integer.MAX_VALUE + 1l | _ Integer.MIN_VALUE - 1l | _ new Object() | _ + } + + @Unroll + def "Int parseValue #value into #result (#result.class)"() { + expect: + Scalars.GraphQLInt.getCoercing().parseValue(value, GraphQLContext.default, Locale.default) == result + where: + value | result + new Integer(42) | 42 + new BigInteger("42") | 42 + new Byte("42") | 42 + new Short("42") | 42 + 1234567l | 1234567 + new AtomicInteger(42) | 42 + Integer.MAX_VALUE | Integer.MAX_VALUE + Integer.MIN_VALUE | Integer.MIN_VALUE + 42.0000d | 42 + new BigDecimal("42") | 42 + 42.0f | 42 + 42.0d | 42 } @Unroll - def "parsValue throws exception for invalid input #value"() { + def "Int parseValue #value into #result (#result.class) with deprecated methods"() { + expect: + Scalars.GraphQLInt.getCoercing().parseValue(value) == result // Retain deprecated for test coverage + + where: + value | result + 42.0000d | 42 + new Integer(42) | 42 + new BigInteger("42") | 42 + new BigDecimal("42") | 42 + 42.0f | 42 + 42.0d | 42 + new Byte("42") | 42 + new Short("42") | 42 + 1234567l | 1234567 + new AtomicInteger(42) | 42 + Integer.MAX_VALUE | Integer.MAX_VALUE + Integer.MIN_VALUE | Integer.MIN_VALUE + } + + @Unroll + def "parseValue throws exception for invalid input #value"() { when: - Scalars.GraphQLInt.getCoercing().parseValue(value) + Scalars.GraphQLInt.getCoercing().parseValue(value, GraphQLContext.default, Locale.default) then: thrown(CoercingParseValueException) - where: value | _ "" | _ @@ -110,7 +184,9 @@ class ScalarsIntTest extends Specification { Integer.MAX_VALUE + 1l | _ Integer.MIN_VALUE - 1l | _ new Object() | _ - + "42" | _ + "42.0000" | _ + "-1" | _ } } diff --git a/src/test/groovy/graphql/ScalarsQuerySchema.java b/src/test/groovy/graphql/ScalarsQuerySchema.java index dcf339afa8..44718be9e3 100644 --- a/src/test/groovy/graphql/ScalarsQuerySchema.java +++ b/src/test/groovy/graphql/ScalarsQuerySchema.java @@ -2,6 +2,8 @@ import graphql.schema.DataFetcher; +import graphql.schema.FieldCoordinates; +import graphql.schema.GraphQLCodeRegistry; import graphql.schema.GraphQLNonNull; import graphql.schema.GraphQLObjectType; import graphql.schema.GraphQLSchema; @@ -12,53 +14,42 @@ public class ScalarsQuerySchema { - public static final DataFetcher inputDF = environment -> environment.getArgument("input"); + public static final DataFetcher inputDF = environment -> environment.getArgument("input"); public static final GraphQLObjectType queryType = newObject() .name("QueryType") - /** Static Scalars */ - .field(newFieldDefinition() - .name("floatNaN") - .type(Scalars.GraphQLFloat) - .staticValue(Double.NaN)) - - - /** Scalars with input of same type, value echoed back */ - .field(newFieldDefinition() - .name("floatNaNInput") - .type(Scalars.GraphQLFloat) - .argument(newArgument() - .name("input") - .type(GraphQLNonNull.nonNull(Scalars.GraphQLFloat))) - .dataFetcher(inputDF)) .field(newFieldDefinition() .name("stringInput") .type(Scalars.GraphQLString) .argument(newArgument() .name("input") - .type(GraphQLNonNull.nonNull(Scalars.GraphQLString))) - .dataFetcher(inputDF)) - - - /** Scalars with input of String, cast to scalar */ + .type(GraphQLNonNull.nonNull(Scalars.GraphQLString)))) + // Scalars with input of String, cast to scalar .field(newFieldDefinition() .name("floatString") .type(Scalars.GraphQLFloat) .argument(newArgument() .name("input") - .type(Scalars.GraphQLString)) - .dataFetcher(inputDF)) + .type(Scalars.GraphQLString))) .field(newFieldDefinition() .name("intString") .type(Scalars.GraphQLInt) .argument(newArgument() .name("input") - .type(Scalars.GraphQLString)) - .dataFetcher(inputDF)) + .type(Scalars.GraphQLString))) .build(); + static FieldCoordinates stringInputCoordinates = FieldCoordinates.coordinates("QueryType", "stringInput"); + static FieldCoordinates floatStringCoordinates = FieldCoordinates.coordinates("QueryType", "floatString"); + static FieldCoordinates intStringCoordinates = FieldCoordinates.coordinates("QueryType", "intString"); + static GraphQLCodeRegistry codeRegistry = GraphQLCodeRegistry.newCodeRegistry() + .dataFetcher(stringInputCoordinates, inputDF) + .dataFetcher(floatStringCoordinates, inputDF) + .dataFetcher(intStringCoordinates, inputDF) + .build(); public static final GraphQLSchema scalarsQuerySchema = GraphQLSchema.newSchema() + .codeRegistry(codeRegistry) .query(queryType) .build(); } diff --git a/src/test/groovy/graphql/ScalarsStringTest.groovy b/src/test/groovy/graphql/ScalarsStringTest.groovy index f25ead1c74..8a725eb122 100644 --- a/src/test/groovy/graphql/ScalarsStringTest.groovy +++ b/src/test/groovy/graphql/ScalarsStringTest.groovy @@ -1,5 +1,6 @@ package graphql +import graphql.execution.CoercedVariables import graphql.language.BooleanValue import graphql.language.StringValue import graphql.schema.CoercingParseLiteralException @@ -10,7 +11,6 @@ import spock.lang.Unroll class ScalarsStringTest extends Specification { - @Shared def customObject = new Object() { @Override @@ -22,18 +22,27 @@ class ScalarsStringTest extends Specification { @Unroll def "String parse literal #literal.value as #result"() { expect: - Scalars.GraphQLString.getCoercing().parseLiteral(literal) == result + Scalars.GraphQLString.getCoercing().parseLiteral(literal, CoercedVariables.emptyVariables(), GraphQLContext.default, Locale.default) == result where: literal | result new StringValue("1234ab") | "1234ab" + } + + @Unroll + def "String parse literal #literal.value as #result with deprecated method"() { + expect: + Scalars.GraphQLString.getCoercing().parseLiteral(literal) == result // Retain deprecated for test coverage + where: + literal | result + new StringValue("1234ab") | "1234ab" } @Unroll def "String returns null for invalid #literal"() { when: - Scalars.GraphQLString.getCoercing().parseLiteral(literal) + Scalars.GraphQLString.getCoercing().parseLiteral(literal, CoercedVariables.emptyVariables(), GraphQLContext.default, Locale.default) then: thrown(CoercingParseLiteralException) @@ -45,8 +54,7 @@ class ScalarsStringTest extends Specification { @Unroll def "String serialize #value into #result (#result.class)"() { expect: - Scalars.GraphQLString.getCoercing().serialize(value) == result - Scalars.GraphQLString.getCoercing().parseValue(value) == result + Scalars.GraphQLString.getCoercing().serialize(value, GraphQLContext.default, Locale.default) == result where: value | result @@ -55,5 +63,47 @@ class ScalarsStringTest extends Specification { customObject | "foo" } + @Unroll + def "String serialize #value into #result (#result.class) with deprecated method"() { + expect: + Scalars.GraphQLString.getCoercing().serialize(value) == result // Retain deprecated method for test coverage + + where: + value | result + "123ab" | "123ab" + 123 | "123" + customObject | "foo" + } + def "String parseValue value into result"() { + expect: + Scalars.GraphQLString.getCoercing().parseValue("123ab", GraphQLContext.default, Locale.default) == "123ab" + } + + def "String parseValue value into result with deprecated method"() { + expect: + Scalars.GraphQLString.getCoercing().parseValue("123ab") == "123ab" // Retain deprecated method for test coverage + } + + @Unroll + def "String parseValue throws exception for non-String values"() { + when: + Scalars.GraphQLString.getCoercing().parseValue(literal, GraphQLContext.default, Locale.default) + then: + def ex = thrown(CoercingParseValueException) + + where: + literal | _ + 123 | _ + true | _ + customObject | _ + } + + def "String parseValue English exception message"() { + when: + Scalars.GraphQLString.getCoercing().parseValue(9001, GraphQLContext.default, Locale.ENGLISH) + then: + def ex = thrown(CoercingParseValueException) + ex.message == "Expected a String input, but it was a 'Integer'" + } } diff --git a/src/test/groovy/graphql/StarWarsData.groovy b/src/test/groovy/graphql/StarWarsData.groovy index 2bfd4accc9..d4a406d565 100644 --- a/src/test/groovy/graphql/StarWarsData.groovy +++ b/src/test/groovy/graphql/StarWarsData.groovy @@ -94,7 +94,6 @@ class StarWarsData { } } - static DataFetcher droidDataFetcher = new DataFetcher() { @Override Object get(DataFetchingEnvironment environment) { diff --git a/src/test/groovy/graphql/StarWarsQueryTest.groovy b/src/test/groovy/graphql/StarWarsQueryTest.groovy index f28cb3fe1b..0ed8b46bb2 100644 --- a/src/test/groovy/graphql/StarWarsQueryTest.groovy +++ b/src/test/groovy/graphql/StarWarsQueryTest.groovy @@ -190,7 +190,8 @@ class StarWarsQueryTest extends Specification { ] ] when: - def result = GraphQL.newGraphQL(StarWarsSchema.starWarsSchema).build().execute(query, null, params).data + def ei = ExecutionInput.newExecutionInput(query).variables(params).build() + def result = GraphQL.newGraphQL(StarWarsSchema.starWarsSchema).build().execute(ei).data then: result == expected @@ -212,7 +213,8 @@ class StarWarsQueryTest extends Specification { human: null ] when: - def result = GraphQL.newGraphQL(StarWarsSchema.starWarsSchema).build().execute(query, null, params).data + def ei = ExecutionInput.newExecutionInput(query).variables(params).build() + def result = GraphQL.newGraphQL(StarWarsSchema.starWarsSchema).build().execute(ei).data then: result == expected diff --git a/src/test/groovy/graphql/StarWarsSchema.java b/src/test/groovy/graphql/StarWarsSchema.java index fe30898bac..bf5b891c02 100644 --- a/src/test/groovy/graphql/StarWarsSchema.java +++ b/src/test/groovy/graphql/StarWarsSchema.java @@ -1,12 +1,14 @@ package graphql; +import graphql.schema.DataFetcher; +import graphql.schema.FieldCoordinates; +import graphql.schema.GraphQLCodeRegistry; import graphql.schema.GraphQLEnumType; import graphql.schema.GraphQLInputObjectType; import graphql.schema.GraphQLInterfaceType; import graphql.schema.GraphQLObjectType; import graphql.schema.GraphQLSchema; -import graphql.schema.GraphqlTypeComparatorRegistry; import graphql.schema.StaticDataFetcher; import static graphql.Scalars.GraphQLString; @@ -24,7 +26,6 @@ public class StarWarsSchema { - public static GraphQLEnumType episodeEnum = newEnum() .name("Episode") .description("One of the films in the Star Wars Trilogy") @@ -54,7 +55,6 @@ public class StarWarsSchema { .name("appearsIn") .description("Which movies they appear in.") .type(list(episodeEnum))) - .typeResolver(StarWarsData.getCharacterTypeResolver()) .comparatorRegistry(BY_NAME_REGISTRY) .build(); @@ -73,8 +73,7 @@ public class StarWarsSchema { .field(newFieldDefinition() .name("friends") .description("The friends of the human, or an empty list if they have none.") - .type(list(characterInterface)) - .dataFetcher(StarWarsData.getFriendsDataFetcher())) + .type(list(characterInterface))) .field(newFieldDefinition() .name("appearsIn") .description("Which movies they appear in.") @@ -101,8 +100,7 @@ public class StarWarsSchema { .field(newFieldDefinition() .name("friends") .description("The friends of the droid, or an empty list if they have none.") - .type(list(characterInterface)) - .dataFetcher(StarWarsData.getFriendsDataFetcher())) + .type(list(characterInterface))) .field(newFieldDefinition() .name("appearsIn") .description("Which movies they appear in.") @@ -132,24 +130,21 @@ public class StarWarsSchema { .argument(newArgument() .name("episode") .description("If omitted, returns the hero of the whole saga. If provided, returns the hero of that particular episode.") - .type(episodeEnum)) - .dataFetcher(new StaticDataFetcher(StarWarsData.getArtoo()))) + .type(episodeEnum))) .field(newFieldDefinition() .name("human") .type(humanType) .argument(newArgument() .name("id") .description("id of the human") - .type(nonNull(GraphQLString))) - .dataFetcher(StarWarsData.getHumanDataFetcher())) + .type(nonNull(GraphQLString)))) .field(newFieldDefinition() .name("droid") .type(droidType) .argument(newArgument() .name("id") .description("id of the droid") - .type(nonNull(GraphQLString))) - .dataFetcher(StarWarsData.getDroidDataFetcher())) + .type(nonNull(GraphQLString)))) .comparatorRegistry(BY_NAME_REGISTRY) .build(); @@ -160,12 +155,35 @@ public class StarWarsSchema { .type(characterInterface) .argument(newArgument() .name("input") - .type(inputHumanType)) - .dataFetcher(new StaticDataFetcher(StarWarsData.getArtoo()))) + .type(inputHumanType))) .comparatorRegistry(BY_NAME_REGISTRY) .build(); + public static FieldCoordinates humanFriendsCoordinates = FieldCoordinates.coordinates("Human", "friends"); + public static DataFetcher humanFriendsDataFetcher = StarWarsData.getFriendsDataFetcher(); + public static FieldCoordinates droidFriendsCoordinates = FieldCoordinates.coordinates("Droid", "friends"); + public static DataFetcher droidFriendsDataFetcher = StarWarsData.getFriendsDataFetcher(); + public static FieldCoordinates heroCoordinates = FieldCoordinates.coordinates("QueryType", "hero"); + public static DataFetcher heroDataFetcher = new StaticDataFetcher(StarWarsData.getArtoo()); + public static FieldCoordinates humanCoordinates = FieldCoordinates.coordinates("QueryType", "human"); + public static DataFetcher humanDataFetcher = StarWarsData.getHumanDataFetcher(); + public static FieldCoordinates droidCoordinates = FieldCoordinates.coordinates("QueryType", "droid"); + public static DataFetcher droidDataFetcher = StarWarsData.getDroidDataFetcher(); + public static FieldCoordinates createHumanCoordinates = FieldCoordinates.coordinates("MutationType", "createHuman"); + public static DataFetcher createHumanDataFetcher = new StaticDataFetcher(StarWarsData.getArtoo()); + + public static GraphQLCodeRegistry codeRegistry = GraphQLCodeRegistry.newCodeRegistry() + .dataFetcher(humanFriendsCoordinates, humanFriendsDataFetcher) + .dataFetcher(droidFriendsCoordinates, droidFriendsDataFetcher) + .dataFetcher(heroCoordinates, heroDataFetcher) + .dataFetcher(humanCoordinates, humanDataFetcher) + .dataFetcher(droidCoordinates, droidDataFetcher) + .dataFetcher(createHumanCoordinates, createHumanDataFetcher) + .typeResolver("Character", StarWarsData.getCharacterTypeResolver()) + .build(); + public static GraphQLSchema starWarsSchema = GraphQLSchema.newSchema() + .codeRegistry(codeRegistry) .query(queryType) .mutation(mutationType) .build(); diff --git a/src/test/groovy/graphql/TestUtil.groovy b/src/test/groovy/graphql/TestUtil.groovy index 3ace9dfea4..35a2ae68b2 100644 --- a/src/test/groovy/graphql/TestUtil.groovy +++ b/src/test/groovy/graphql/TestUtil.groovy @@ -103,18 +103,18 @@ class TestUtil { schema(specReader, mockRuntimeWiring) } - static GraphQLSchema schema(String spec, RuntimeWiring runtimeWiring) { - schema(new StringReader(spec), runtimeWiring) + static GraphQLSchema schema(String spec, RuntimeWiring runtimeWiring, boolean commentsAsDescription = true) { + schema(new StringReader(spec), runtimeWiring, commentsAsDescription) } static GraphQLSchema schema(InputStream specStream, RuntimeWiring runtimeWiring) { schema(new InputStreamReader(specStream), runtimeWiring) } - static GraphQLSchema schema(Reader specReader, RuntimeWiring runtimeWiring) { + static GraphQLSchema schema(Reader specReader, RuntimeWiring runtimeWiring, boolean commentsAsDescription = true) { try { def registry = new SchemaParser().parse(specReader) - def options = SchemaGenerator.Options.defaultOptions() + def options = SchemaGenerator.Options.defaultOptions().useCommentsAsDescriptions(commentsAsDescription) return new SchemaGenerator().makeExecutableSchema(options, registry, runtimeWiring) } catch (SchemaProblem e) { assert false: "The schema could not be compiled : ${e}" diff --git a/src/test/groovy/graphql/TypeMismatchErrorTest.groovy b/src/test/groovy/graphql/TypeMismatchErrorTest.groovy index 467c21c57a..ff226aab8b 100644 --- a/src/test/groovy/graphql/TypeMismatchErrorTest.groovy +++ b/src/test/groovy/graphql/TypeMismatchErrorTest.groovy @@ -2,7 +2,6 @@ package graphql import graphql.introspection.Introspection import graphql.schema.GraphQLType -import graphql.schema.TypeResolverProxy import spock.lang.Specification import spock.lang.Unroll @@ -22,14 +21,14 @@ class TypeMismatchErrorTest extends Specification { TypeMismatchError.GraphQLTypeToTypeKindMapping.getTypeKindFromGraphQLType(type) == typeKind where: - type || typeKind - list(Scalars.GraphQLInt) || Introspection.TypeKind.LIST - Scalars.GraphQLInt || Introspection.TypeKind.SCALAR - newObject().name("myObject").fields([]).build() || Introspection.TypeKind.OBJECT - newEnum().name("myEnum").values([]).build() || Introspection.TypeKind.ENUM - newInputObject().name("myInputType").fields([]).build() || Introspection.TypeKind.INPUT_OBJECT - newInterface().name("myInterfaceType").fields([]).typeResolver(new TypeResolverProxy()).build() || Introspection.TypeKind.INTERFACE - nonNull(Scalars.GraphQLInt) || Introspection.TypeKind.NON_NULL - newUnionType().name("myUnion").possibleType(newObject().name("test").build()).build() || Introspection.TypeKind.UNION + type || typeKind + list(Scalars.GraphQLInt) || Introspection.TypeKind.LIST + Scalars.GraphQLInt || Introspection.TypeKind.SCALAR + newObject().name("myObject").fields([]).build() || Introspection.TypeKind.OBJECT + newEnum().name("myEnum").values([]).build() || Introspection.TypeKind.ENUM + newInputObject().name("myInputType").fields([]).build() || Introspection.TypeKind.INPUT_OBJECT + newInterface().name("myInterfaceType").fields([]).build() || Introspection.TypeKind.INTERFACE + nonNull(Scalars.GraphQLInt) || Introspection.TypeKind.NON_NULL + newUnionType().name("myUnion").possibleType(newObject().name("test").build()).build() || Introspection.TypeKind.UNION } } diff --git a/src/test/groovy/graphql/TypeReferenceSchema.java b/src/test/groovy/graphql/TypeReferenceSchema.java index 22788c3699..37c48a2841 100644 --- a/src/test/groovy/graphql/TypeReferenceSchema.java +++ b/src/test/groovy/graphql/TypeReferenceSchema.java @@ -2,6 +2,7 @@ import graphql.schema.Coercing; import graphql.schema.GraphQLArgument; +import graphql.schema.GraphQLCodeRegistry; import graphql.schema.GraphQLDirective; import graphql.schema.GraphQLEnumType; import graphql.schema.GraphQLFieldDefinition; @@ -148,7 +149,6 @@ public Boolean parseLiteral(Object input) { .name("Pet") .possibleType(GraphQLTypeReference.typeRef(CatType.getName())) .possibleType(GraphQLTypeReference.typeRef(DogType.getName())) - .typeResolver(new TypeResolverProxy()) .withDirective(unionDirective) .build(); } @@ -171,7 +171,6 @@ public Boolean parseLiteral(Object input) { Addressable = GraphQLInterfaceType.newInterface() .name("Addressable") - .typeResolver(new TypeResolverProxy()) .field(GraphQLFieldDefinition.newFieldDefinition() .name("address") .type(GraphQLString)) @@ -316,6 +315,12 @@ public Boolean parseLiteral(Object input) { .type(QueryDirectiveInput)) .build(); + public static GraphQLCodeRegistry codeRegistry = GraphQLCodeRegistry.newCodeRegistry() + .typeResolver("Pet", new TypeResolverProxy()) + .typeResolver("Addressable", new TypeResolverProxy()) + .typeResolver("Named", GarfieldSchema.namedTypeResolver) + .build(); + public static GraphQLSchema SchemaWithReferences = GraphQLSchema.newSchema() .query(PersonService) .additionalTypes(new HashSet<>(Arrays.asList(PersonType, PersonInputType, PetType, CatType, DogType, NamedType, HairStyle, OnOff))) @@ -330,6 +335,6 @@ public Boolean parseLiteral(Object input) { .additionalDirective(enumDirective) .additionalDirective(enumValueDirective) .additionalDirective(interfaceDirective) - + .codeRegistry(codeRegistry) .build(); } diff --git a/src/test/groovy/graphql/TypeResolutionEnvironmentTest.groovy b/src/test/groovy/graphql/TypeResolutionEnvironmentTest.groovy index 70605beb1a..55be8cc947 100644 --- a/src/test/groovy/graphql/TypeResolutionEnvironmentTest.groovy +++ b/src/test/groovy/graphql/TypeResolutionEnvironmentTest.groovy @@ -51,7 +51,7 @@ class TypeResolutionEnvironmentTest extends Specification { .field(mergedField(new Field("field"))) .fieldType(interfaceType) .schema(schema) - .context("FooBar") + .context("FooBar") // Retain for test coverage .graphQLContext(graphqlContext) .localContext("LocalContext") .build() @@ -65,7 +65,7 @@ class TypeResolutionEnvironmentTest extends Specification { assert source == "source" assert env.getField().getName() == "field" assert env.getFieldType() == interfaceType - assert env.getContext() == "FooBar" + assert env.getContext() == "FooBar" // Retain for test coverage assert env.getLocalContext() == "LocalContext" assert env.getGraphQLContext() == graphqlContext assert env.getArguments() == [a: "b"] @@ -104,10 +104,10 @@ class TypeResolutionEnvironmentTest extends Specification { String source = env.getObject() assert source == "source" assert env.getGraphQLContext().get("a") == "b" - if ("FooBar" == env.getContext()) { + if ("FooBar" == env.getContext()) { // Retain for test coverage return schema.getObjectType("FooBar") } - if ("Foo" == env.getContext()) { + if ("Foo" == env.getContext()) { // Retain for test coverage return schema.getObjectType("FooImpl") } return null @@ -121,7 +121,7 @@ class TypeResolutionEnvironmentTest extends Specification { .field(mergedField(new Field("field"))) .fieldType(interfaceType) .schema(schema) - .context("FooBar") + .context("FooBar") // Retain for test coverage .graphQLContext(graphqlContext) .build() @@ -137,7 +137,7 @@ class TypeResolutionEnvironmentTest extends Specification { .field(mergedField(new Field("field"))) .fieldType(interfaceType) .schema(schema) - .context("Foo") + .context("Foo") // Retain for test coverage .graphQLContext(graphqlContext) .build() diff --git a/src/test/groovy/graphql/TypeResolverExecutionTest.groovy b/src/test/groovy/graphql/TypeResolverExecutionTest.groovy index 4e4ae2f693..b19cf59a15 100644 --- a/src/test/groovy/graphql/TypeResolverExecutionTest.groovy +++ b/src/test/groovy/graphql/TypeResolverExecutionTest.groovy @@ -612,14 +612,12 @@ class TypeResolverExecutionTest extends Specification { GraphQLObjectType getType(TypeResolutionEnvironment env) { assert env.getField().getName() == "foo" assert env.getFieldType() == env.getSchema().getType("BarInterface") - assert env.getContext() == "Context" assert env.getGraphQLContext().get("x") == "graphqlContext" assert env.getLocalContext() == "LocalContext" return env.getSchema().getObjectType("NewBar") } } - def df = { env -> DataFetcherResult.newResult().data([name: "NAME"]).localContext("LocalContext").build() } @@ -648,7 +646,6 @@ class TypeResolverExecutionTest extends Specification { when: def ei = newExecutionInput(query) - .context("Context") .graphQLContext(["x" : "graphqlContext"]) .build() def er = graphQL.execute(ei) diff --git a/src/test/groovy/graphql/UnionTest.groovy b/src/test/groovy/graphql/UnionTest.groovy index 4fb9f2af25..403f31d3d8 100644 --- a/src/test/groovy/graphql/UnionTest.groovy +++ b/src/test/groovy/graphql/UnionTest.groovy @@ -4,7 +4,6 @@ import spock.lang.Specification class UnionTest extends Specification { - def "can introspect on union and intersection types"() { def query = """ { @@ -96,7 +95,8 @@ class UnionTest extends Specification { ] when: - def executionResult = GraphQL.newGraphQL(GarfieldSchema.GarfieldSchema).build().execute(query, GarfieldSchema.john) + def ei = ExecutionInput.newExecutionInput(query).root(GarfieldSchema.john).build() + def executionResult = GraphQL.newGraphQL(GarfieldSchema.GarfieldSchema).build().execute(ei) then: executionResult.data == expectedResult @@ -148,7 +148,8 @@ class UnionTest extends Specification { ] ] when: - def executionResult = GraphQL.newGraphQL(GarfieldSchema.GarfieldSchema).build().execute(query, GarfieldSchema.john) + def ei = ExecutionInput.newExecutionInput(query).root(GarfieldSchema.john).build() + def executionResult = GraphQL.newGraphQL(GarfieldSchema.GarfieldSchema).build().execute(ei) then: executionResult.data == expectedResult diff --git a/src/test/groovy/graphql/analysis/MaxQueryComplexityInstrumentationTest.groovy b/src/test/groovy/graphql/analysis/MaxQueryComplexityInstrumentationTest.groovy index 3670b4fe1d..cee882ccae 100644 --- a/src/test/groovy/graphql/analysis/MaxQueryComplexityInstrumentationTest.groovy +++ b/src/test/groovy/graphql/analysis/MaxQueryComplexityInstrumentationTest.groovy @@ -6,6 +6,7 @@ import graphql.execution.AbortExecutionException import graphql.execution.ExecutionContext import graphql.execution.ExecutionContextBuilder import graphql.execution.ExecutionId +import graphql.execution.instrumentation.InstrumentationState import graphql.execution.instrumentation.parameters.InstrumentationExecuteOperationParameters import graphql.execution.instrumentation.parameters.InstrumentationValidationParameters import graphql.language.Document @@ -40,9 +41,10 @@ class MaxQueryComplexityInstrumentationTest extends Specification { """) MaxQueryComplexityInstrumentation queryComplexityInstrumentation = new MaxQueryComplexityInstrumentation(10) ExecutionInput executionInput = Mock(ExecutionInput) - InstrumentationExecuteOperationParameters executeOperationParameters = createExecuteOperationParameters(queryComplexityInstrumentation, executionInput, query, schema) + def state = createInstrumentationState(queryComplexityInstrumentation) + InstrumentationExecuteOperationParameters executeOperationParameters = createExecuteOperationParameters(queryComplexityInstrumentation, executionInput, query, schema, state) when: - queryComplexityInstrumentation.beginExecuteOperation(executeOperationParameters) + queryComplexityInstrumentation.beginExecuteOperation(executeOperationParameters, state) then: def e = thrown(AbortExecutionException) e.message == "maximum query complexity exceeded 11 > 10" @@ -62,9 +64,10 @@ class MaxQueryComplexityInstrumentationTest extends Specification { """) MaxQueryComplexityInstrumentation queryComplexityInstrumentation = new MaxQueryComplexityInstrumentation(1) ExecutionInput executionInput = Mock(ExecutionInput) - InstrumentationExecuteOperationParameters executeOperationParameters = createExecuteOperationParameters(queryComplexityInstrumentation, executionInput, query, schema) + def state = createInstrumentationState(queryComplexityInstrumentation) + InstrumentationExecuteOperationParameters executeOperationParameters = createExecuteOperationParameters(queryComplexityInstrumentation, executionInput, query, schema, state) when: - queryComplexityInstrumentation.beginExecuteOperation(executeOperationParameters) + queryComplexityInstrumentation.beginExecuteOperation(executeOperationParameters, state) then: def e = thrown(AbortExecutionException) e.message == "maximum query complexity exceeded 2 > 1" @@ -89,9 +92,10 @@ class MaxQueryComplexityInstrumentationTest extends Specification { def calculator = Mock(FieldComplexityCalculator) MaxQueryComplexityInstrumentation queryComplexityInstrumentation = new MaxQueryComplexityInstrumentation(5, calculator) ExecutionInput executionInput = Mock(ExecutionInput) - InstrumentationExecuteOperationParameters executeOperationParameters = createExecuteOperationParameters(queryComplexityInstrumentation, executionInput, query, schema) + def state = createInstrumentationState(queryComplexityInstrumentation) + InstrumentationExecuteOperationParameters executeOperationParameters = createExecuteOperationParameters(queryComplexityInstrumentation, executionInput, query, schema, state) when: - queryComplexityInstrumentation.beginExecuteOperation(executeOperationParameters) + queryComplexityInstrumentation.beginExecuteOperation(executeOperationParameters, state) then: 1 * calculator.calculate({ FieldComplexityEnvironment env -> env.field.name == "scalar" }, 0) >> 10 @@ -128,9 +132,10 @@ class MaxQueryComplexityInstrumentationTest extends Specification { } MaxQueryComplexityInstrumentation queryComplexityInstrumentation = new MaxQueryComplexityInstrumentation(10, maxQueryComplexityExceededFunction) ExecutionInput executionInput = Mock(ExecutionInput) - InstrumentationExecuteOperationParameters executeOperationParameters = createExecuteOperationParameters(queryComplexityInstrumentation, executionInput, query, schema) + def state = createInstrumentationState(queryComplexityInstrumentation) + InstrumentationExecuteOperationParameters executeOperationParameters = createExecuteOperationParameters(queryComplexityInstrumentation, executionInput, query, schema, state) when: - queryComplexityInstrumentation.beginExecuteOperation(executeOperationParameters) + queryComplexityInstrumentation.beginExecuteOperation(executeOperationParameters, state) then: customFunctionCalled notThrown(Exception) @@ -151,24 +156,29 @@ class MaxQueryComplexityInstrumentationTest extends Specification { MaxQueryComplexityInstrumentation queryComplexityInstrumentation = new MaxQueryComplexityInstrumentation(0) ExecutionInput executionInput = Mock(ExecutionInput) - InstrumentationExecuteOperationParameters executeOperationParameters = createExecuteOperationParameters(queryComplexityInstrumentation, executionInput, query, schema) + def state = createInstrumentationState(queryComplexityInstrumentation) + InstrumentationExecuteOperationParameters executeOperationParameters = createExecuteOperationParameters(queryComplexityInstrumentation, executionInput, query, schema, state) when: - queryComplexityInstrumentation.beginExecuteOperation(executeOperationParameters) + queryComplexityInstrumentation.beginExecuteOperation(executeOperationParameters, state) then: def e = thrown(AbortExecutionException) e.message == "maximum query complexity exceeded 1 > 0" } - private InstrumentationExecuteOperationParameters createExecuteOperationParameters(MaxQueryComplexityInstrumentation queryComplexityInstrumentation, ExecutionInput executionInput, Document query, GraphQLSchema schema) { + private InstrumentationExecuteOperationParameters createExecuteOperationParameters(MaxQueryComplexityInstrumentation queryComplexityInstrumentation, ExecutionInput executionInput, Document query, GraphQLSchema schema, InstrumentationState state) { // we need to run N steps to create instrumentation state - def instrumentationState = queryComplexityInstrumentation.createState(null) - def validationParameters = new InstrumentationValidationParameters(executionInput, query, schema, instrumentationState) - queryComplexityInstrumentation.beginValidation(validationParameters) + def validationParameters = new InstrumentationValidationParameters(executionInput, query, schema, state) + queryComplexityInstrumentation.beginValidation(validationParameters, state) def executionContext = executionCtx(executionInput, query, schema) - def executeOperationParameters = new InstrumentationExecuteOperationParameters(executionContext).withNewState(instrumentationState) + def executeOperationParameters = new InstrumentationExecuteOperationParameters(executionContext) executeOperationParameters } + def createInstrumentationState(MaxQueryComplexityInstrumentation queryComplexityInstrumentation) { + queryComplexityInstrumentation.createState(null) + } + + private ExecutionContext executionCtx(ExecutionInput executionInput, Document query, GraphQLSchema schema) { ExecutionContextBuilder.newExecutionContextBuilder() .executionInput(executionInput).document(query).graphQLSchema(schema).executionId(ExecutionId.generate()) diff --git a/src/test/groovy/graphql/analysis/MaxQueryDepthInstrumentationTest.groovy b/src/test/groovy/graphql/analysis/MaxQueryDepthInstrumentationTest.groovy index 11ac79bc7f..942bee10dc 100644 --- a/src/test/groovy/graphql/analysis/MaxQueryDepthInstrumentationTest.groovy +++ b/src/test/groovy/graphql/analysis/MaxQueryDepthInstrumentationTest.groovy @@ -17,12 +17,11 @@ import java.util.function.Function class MaxQueryDepthInstrumentationTest extends Specification { - Document createQuery(String query) { + static Document createQuery(String query) { Parser parser = new Parser() parser.parseDocument(query) } - def "throws exception if too deep"() { given: def schema = TestUtil.schema(""" @@ -43,13 +42,39 @@ class MaxQueryDepthInstrumentationTest extends Specification { def executionContext = executionCtx(executionInput, query, schema) def executeOperationParameters = new InstrumentationExecuteOperationParameters(executionContext) when: - maximumQueryDepthInstrumentation.beginExecuteOperation(executeOperationParameters) + maximumQueryDepthInstrumentation.beginExecuteOperation(executeOperationParameters, null) then: def e = thrown(AbortExecutionException) e.message.contains("maximum query depth exceeded 7 > 6") } def "doesn't throw exception if not deep enough"() { + given: + def schema = TestUtil.schema(""" + type Query{ + foo: Foo + bar: String + } + type Foo { + scalar: String + foo: Foo + } + """) + def query = createQuery(""" + {f1: foo {foo {foo {scalar}}} f2: foo { foo {foo {foo {foo{foo{scalar}}}}}} } + """) + MaxQueryDepthInstrumentation maximumQueryDepthInstrumentation = new MaxQueryDepthInstrumentation(7) + ExecutionInput executionInput = Mock(ExecutionInput) + def executionContext = executionCtx(executionInput, query, schema) + def executeOperationParameters = new InstrumentationExecuteOperationParameters(executionContext) + def state = maximumQueryDepthInstrumentation.createState(null) + when: + maximumQueryDepthInstrumentation.beginExecuteOperation(executeOperationParameters, state) + then: + notThrown(Exception) + } + + def "doesn't throw exception if not deep enough with deprecated beginExecuteOperation"() { given: def schema = TestUtil.schema(""" type Query{ @@ -69,7 +94,7 @@ class MaxQueryDepthInstrumentationTest extends Specification { def executionContext = executionCtx(executionInput, query, schema) def executeOperationParameters = new InstrumentationExecuteOperationParameters(executionContext) when: - maximumQueryDepthInstrumentation.beginExecuteOperation(executeOperationParameters) + maximumQueryDepthInstrumentation.beginExecuteOperation(executeOperationParameters, null) // Retain for test coverage then: notThrown(Exception) } @@ -102,7 +127,7 @@ class MaxQueryDepthInstrumentationTest extends Specification { def executionContext = executionCtx(executionInput, query, schema) def executeOperationParameters = new InstrumentationExecuteOperationParameters(executionContext) when: - maximumQueryDepthInstrumentation.beginExecuteOperation(executeOperationParameters) + maximumQueryDepthInstrumentation.beginExecuteOperation(executeOperationParameters, null) then: calledFunction notThrown(Exception) @@ -133,7 +158,7 @@ class MaxQueryDepthInstrumentationTest extends Specification { !er.errors.isEmpty() } - private ExecutionContext executionCtx(ExecutionInput executionInput, Document query, GraphQLSchema schema) { + static private ExecutionContext executionCtx(ExecutionInput executionInput, Document query, GraphQLSchema schema) { ExecutionContextBuilder.newExecutionContextBuilder() .executionInput(executionInput).document(query).graphQLSchema(schema).executionId(ExecutionId.generate()).build() } diff --git a/src/test/groovy/graphql/analysis/values/ValueTraverserTest.groovy b/src/test/groovy/graphql/analysis/values/ValueTraverserTest.groovy new file mode 100644 index 0000000000..dabb380e53 --- /dev/null +++ b/src/test/groovy/graphql/analysis/values/ValueTraverserTest.groovy @@ -0,0 +1,945 @@ +package graphql.analysis.values + +import graphql.AssertException +import graphql.ExecutionInput +import graphql.GraphQL +import graphql.TestUtil +import graphql.schema.DataFetcher +import graphql.schema.DataFetchingEnvironment +import graphql.schema.DataFetchingEnvironmentImpl +import graphql.schema.GraphQLAppliedDirectiveArgument +import graphql.schema.GraphQLArgument +import graphql.schema.GraphQLEnumType +import graphql.schema.GraphQLFieldDefinition +import graphql.schema.GraphQLFieldsContainer +import graphql.schema.GraphQLInputObjectField +import graphql.schema.GraphQLInputObjectType +import graphql.schema.GraphQLInputSchemaElement +import graphql.schema.GraphQLList +import graphql.schema.GraphQLNamedSchemaElement +import graphql.schema.GraphQLScalarType +import graphql.schema.idl.SchemaDirectiveWiring +import graphql.schema.idl.SchemaDirectiveWiringEnvironment +import org.jetbrains.annotations.Nullable +import spock.lang.Specification + +import static graphql.schema.idl.RuntimeWiring.newRuntimeWiring +import static graphql.schema.idl.TypeRuntimeWiring.newTypeWiring + +class ValueTraverserTest extends Specification { + + def sdl = """ + type Query { + field(arg1 : ComplexInput, arg2 : ComplexInput, stringArg : String, enumArg : RGB) : String + } + + input ComplexInput { + complexListField : [[ComplexInput!]] + objectField : InnerInput + listField : [Int!] + stringField : String + enumField : RGB + } + + input InnerInput { + innerListField : [Int!] + innerStringField : String + innerEnumField : RGB + } + + + enum RGB { + RED,GREEN,BLUE + } + """ + + def schema = TestUtil.schema(sdl) + + class CountingVisitor implements ValueVisitor { + + Map visits = [:] + + private int bumpCount(String name) { + visits.compute(name, { k, v -> return v == null ? 0 : ++v }) + } + + @Override + Object visitScalarValue(Object coercedValue, GraphQLScalarType inputType, InputElements inputElements) { + bumpCount("scalar") + return coercedValue + } + + @Override + Object visitEnumValue(Object coercedValue, GraphQLEnumType inputType, InputElements inputElements) { + bumpCount("enum") + return coercedValue + } + + @Override + Object visitInputObjectFieldValue(Object coercedValue, GraphQLInputObjectType inputObjectType, GraphQLInputObjectField inputObjectField, InputElements inputElements) { + bumpCount("objectField") + return coercedValue + } + + @Override + Map visitInputObjectValue(Map coercedValue, GraphQLInputObjectType inputObjectType, InputElements inputElements) { + bumpCount("object") + return coercedValue + } + + @Override + List visitListValue(List coercedValue, GraphQLList listInputType, InputElements inputElements) { + bumpCount("list") + return coercedValue + } + } + + def "can traverse and changes nothing at all"() { + + def fieldDef = this.schema.getObjectType("Query").getFieldDefinition("field") + + def innerInput = [ + innerListField : [6, 7, 8], + innerStringField: "Inner", + innerEnumField : "RED", + ] + def complexInput = [ + complexListField: [[[objectField: innerInput, complexListField: [[]], stringField: "There", enumField: "GREEN"]]], + objectField : [innerStringField: "World", innerEnumField: "BLUE"], + listField : [1, 2, 3, 4, 5], + stringField : "Hello", + enumField : "RED", + ] + Map originalValues = [ + arg1 : complexInput, + arg2 : null, + stringArg : "Hello", + enumArg : "RGB", + noFieldData: "wat", + ] + def visitor = new CountingVisitor() + when: + def newValues = ValueTraverser.visitPreOrder(originalValues, fieldDef, visitor) + then: + // nothing changed - its the same object + newValues === originalValues + visitor.visits == ["scalar": 12, "enum": 4, "list": 5, "object": 4, "objectField": 13] + + + when: + def originalDFE = DataFetchingEnvironmentImpl.newDataFetchingEnvironment().fieldDefinition(fieldDef).graphQLSchema(this.schema).arguments(originalValues).build() + def newDFE = ValueTraverser.visitPreOrder(originalDFE, visitor) + + then: + newDFE === originalDFE + + when: + def graphQLArgument = fieldDef.getArgument("arg1") + def newValue = ValueTraverser.visitPreOrder(complexInput, graphQLArgument, visitor) + + then: + complexInput === newValue + } + + def "can change simple values"() { + def fieldDef = this.schema.getObjectType("Query").getFieldDefinition("field") + + def innerInput = [ + innerListField : [6, 7, 8], + innerStringField: "Inner", + innerEnumField : "RED", + ] + def complexInput = [ + complexListField: [[[objectField: innerInput, complexListField: [[]], stringField: "There", enumField: "GREEN"]]], + objectField : [innerStringField: "World", innerEnumField: "BLUE"], + listField : [1, 2, 3, 4, 5], + stringField : "Hello", + enumField : "RED", + ] + Map originalValues = [ + arg1 : complexInput, + arg2 : null, + stringArg : "Hello", + enumArg : "BLUE", + noFieldData: "wat", + ] + def visitor = new ValueVisitor() { + @Override + Object visitScalarValue(Object coercedValue, GraphQLScalarType inputType, ValueVisitor.InputElements inputElements) { + if (coercedValue instanceof String) { + def val = coercedValue as String + return val.toUpperCase().reverse() + } + if (coercedValue instanceof Number) { + return coercedValue * 1000 + } + return coercedValue; + } + + @Override + Object visitEnumValue(Object coercedValue, GraphQLEnumType inputType, ValueVisitor.InputElements inputElements) { + def val = coercedValue as String + return val.toLowerCase().reverse() + } + } + when: + def newValues = ValueTraverser.visitPreOrder(originalValues, fieldDef, visitor) + then: + // numbers are 1000 greater and strings are upper case reversed and enums are lower cased reversed + newValues == [ + arg1 : [complexListField: [[[ + objectField : [ + innerListField : [6000, 7000, 8000], + innerStringField: "RENNI", + innerEnumField : "der" + ], + complexListField: [[]], + stringField : "EREHT", + enumField : "neerg"]]], + objectField : [innerStringField: "DLROW", innerEnumField: "eulb"], + listField : [1000, 2000, 3000, 4000, 5000], + stringField : "OLLEH", + enumField : "der"], + arg2 : null, + stringArg : "OLLEH", + enumArg : "eulb", + noFieldData: "wat" + ] + } + + def "can change a list midway through "() { + def sdl = """ + type Query { + field(arg : [Int]!) : String + } + """ + def schema = TestUtil.schema(sdl) + + def fieldDef = schema.getObjectType("Query").getFieldDefinition("field") + def argValues = [arg: [1, 2, 3, 4]] + def visitor = new ValueVisitor() { + @Override + Object visitScalarValue(Object coercedValue, GraphQLScalarType inputType, ValueVisitor.InputElements inputElements) { + if (coercedValue == 3) { + return 33 + } + return coercedValue + } + } + when: + def newValues = ValueTraverser.visitPreOrder(argValues, fieldDef, visitor) + then: + newValues == [arg: [1, 2, 33, 4]] + } + + def "can change an object midway through "() { + def sdl = """ + type Query { + field(arg : Input!) : String + } + + input Input { + name : String + age : Int + input : Input + } + """ + def schema = TestUtil.schema(sdl) + + def fieldDef = schema.getObjectType("Query").getFieldDefinition("field") + def argValues = [arg: + [name: "Tess", age: 42, input: + [name: "Tom", age: 33, input: + [name: "Ted", age: 42]] + ] + ] + def visitor = new ValueVisitor() { + @Override + Object visitScalarValue(Object coercedValue, GraphQLScalarType inputType, ValueVisitor.InputElements inputElements) { + if (coercedValue == 42) { + return 24 + } + return coercedValue + } + } + when: + def actual = ValueTraverser.visitPreOrder(argValues, fieldDef, visitor) + + def expected = [arg: + [name: "Tess", age: 24, input: + [name: "Tom", age: 33, input: + [name: "Ted", age: 24] + ] + ] + ] + then: + actual == expected + + + // can change a DFE arguments + when: + def startingDFE = DataFetchingEnvironmentImpl.newDataFetchingEnvironment().fieldDefinition(fieldDef).arguments(argValues).build() + def newDFE = ValueTraverser.visitPreOrder(startingDFE, visitor) + + then: + newDFE.getArguments() == expected + newDFE.getFieldDefinition() == fieldDef + + // can change a single arguments + when: + def newValues = ValueTraverser.visitPreOrder(argValues['arg'], fieldDef.getArgument("arg"), visitor) + + then: + newValues == [name: "Tess", age: 24, input: + [name: "Tom", age: 33, input: + [name: "Ted", age: 24] + ] + ] + } + + def "can visit arguments and change things"() { + def sdl = """ + type Query { + field(arg1 : Input!, arg2 : Input, removeArg : Input) : String + } + + input Input { + name : String + age : Int + input : Input + } + """ + def schema = TestUtil.schema(sdl) + + def fieldDef = schema.getObjectType("Query").getFieldDefinition("field") + def argValues = [ + arg1 : + [name: "Tess", age: 42], + arg2 : + [name: "Tom", age: 24], + removeArg: + [name: "Gone-ski", age: 99], + ] + def visitor = new ValueVisitor() { + @Override + Object visitArgumentValue(@Nullable Object coercedValue, GraphQLArgument graphQLArgument, ValueVisitor.InputElements inputElements) { + if (graphQLArgument.name == "arg2") { + return [name: "Harry Potter", age: 54] + } + if (graphQLArgument.name == "removeArg") { + return ABSENCE_SENTINEL + } + return coercedValue + } + } + when: + def actual = ValueTraverser.visitPreOrder(argValues, fieldDef, visitor) + + def expected = [ + arg1: + [name: "Tess", age: 42], + arg2: + [name: "Harry Potter", age: 54] + ] + then: + actual == expected + + + // can change a DFE arguments + when: + def startingDFE = DataFetchingEnvironmentImpl.newDataFetchingEnvironment().fieldDefinition(fieldDef).arguments(argValues).build() + def newDFE = ValueTraverser.visitPreOrder(startingDFE, visitor) + + then: + newDFE.getArguments() == expected + newDFE.getFieldDefinition() == fieldDef + + // can change a single arguments + when: + def newValues = ValueTraverser.visitPreOrder(argValues['arg2'], fieldDef.getArgument("arg2"), visitor) + + then: + newValues == [name: "Harry Potter", age: 54] + + // catches non sense states + when: + ValueTraverser.visitPreOrder([:], fieldDef.getArgument("removeArg"), visitor) + + then: + thrown(AssertException.class) + } + + def "can handle applied directive arguments"() { + def sdl = """ + directive @d( + arg1 : Input + arg2 : Input + removeArg : Input + ) on FIELD_DEFINITION + + type Query { + field : String @d( + arg1: + {name: "Tom Riddle", age: 42} + arg2: + {name: "Ron Weasley", age: 42} + removeArg: + {name: "Ron Weasley", age: 42} + ) + } + + input Input { + name : String + age : Int + input : Input + } + """ + def schema = TestUtil.schema(sdl) + + def fieldDef = schema.getObjectType("Query").getFieldDefinition("field") + def appliedDirective = fieldDef.getAppliedDirective("d") + def visitor = new ValueVisitor() { + + @Override + Object visitScalarValue(@Nullable Object coercedValue, GraphQLScalarType inputType, ValueVisitor.InputElements inputElements) { + if (coercedValue == "Tom Riddle") { + return "Happy Potter" + } + return coercedValue + } + + @Override + Object visitAppliedDirectiveArgumentValue(@Nullable Object coercedValue, GraphQLAppliedDirectiveArgument graphQLAppliedDirectiveArgument, ValueVisitor.InputElements inputElements) { + if (graphQLAppliedDirectiveArgument.name == "arg2") { + return [name: "Harry Potter", age: 54] + } + if (graphQLAppliedDirectiveArgument.name == "removeArg") { + return ABSENCE_SENTINEL + } + return coercedValue + } + } + + + def appliedDirectiveArgument = appliedDirective.getArgument("arg1") + when: + def actual = ValueTraverser.visitPreOrder(appliedDirectiveArgument.getValue(), appliedDirectiveArgument, visitor) + + then: + actual == [name: "Happy Potter", age: 42] + + when: + appliedDirectiveArgument = appliedDirective.getArgument("arg2") + actual = ValueTraverser.visitPreOrder(appliedDirectiveArgument.getValue(), appliedDirectiveArgument, visitor) + + then: + actual == [name: "Harry Potter", age: 54] + + + // catches non sense states + when: + appliedDirectiveArgument = appliedDirective.getArgument("removeArg") + ValueTraverser.visitPreOrder(appliedDirectiveArgument.getValue(), appliedDirectiveArgument, visitor) + + then: + thrown(AssertException.class) + } + + def "can handle a null changes"() { + def sdl = """ + type Query { + field(arg : Input!) : String + } + + input Input { + listField : [String!] + objectField : Input + stringField : String + leaveAloneField : String + } + """ + def schema = TestUtil.schema(sdl) + + def fieldDef = schema.getObjectType("Query").getFieldDefinition("field") + def argValues = [arg: + [ + listField : ["a", "b", "c"], + objectField : [listField: ["a", "b", "c"]], + stringField : "s", + leaveAloneField: "ok" + ] + ] + def visitor = new ValueVisitor() { + + @Override + Object visitInputObjectFieldValue(Object coercedValue, GraphQLInputObjectType inputObjectType, GraphQLInputObjectField inputObjectField, ValueVisitor.InputElements inputElements) { + if (inputObjectField.name == "leaveAloneField") { + return coercedValue + } + return null + } + + } + when: + def actual = ValueTraverser.visitPreOrder(argValues, fieldDef, visitor) + + def expected = [arg: + [ + listField : null, + objectField : null, + stringField : null, + leaveAloneField: "ok", + ] + ] + then: + actual == expected + } + + def "can turn nulls into actual values"() { + def sdl = """ + type Query { + field(arg : Input) : String + } + + input Input { + listField : [String] + objectField : Input + stringField : String + } + """ + def schema = TestUtil.schema(sdl) + + def fieldDef = schema.getObjectType("Query").getFieldDefinition("field") + def argValues = [arg: + [ + listField : null, + objectField: null, + stringField: null, + ] + ] + def visitor = new ValueVisitor() { + + @Override + Object visitInputObjectFieldValue(Object coercedValue, GraphQLInputObjectType inputObjectType, GraphQLInputObjectField inputObjectField, ValueVisitor.InputElements inputElements) { + if (inputObjectField.name == "listField") { + return ["a", "b", "c"] + } + if (inputObjectField.name == "objectField") { + return [listField: ["x", "y", "z"], stringField: ["will be overwritten"]] + } + if (inputObjectField.name == "stringField") { + return "stringValue" + } + return coercedValue + } + + } + when: + def actual = ValueTraverser.visitPreOrder(argValues, fieldDef, visitor) + + def expected = [arg: + [ + listField : ["a", "b", "c"], + objectField: [listField: ["a", "b", "c"], stringField: "stringValue"], + stringField: "stringValue", + ] + ] + then: + actual == expected + } + + + def "can use the sentinel to remove elements"() { + def sdl = """ + + type Query { + field(arg : Input!, arg2 : String) : String + } + + input Input { + name : String + age : Int + extraInput : ExtraInput + listInput : [Int] + } + + input ExtraInput { + name : String + gone : Boolean + age : Int + otherInput : ExtraInput + } + """ + def schema = TestUtil.schema(sdl) + + def fieldDef = schema.getObjectType("Query").getFieldDefinition("field") + def argValues = [arg : + [name : "Tess", + age : 42, + extraInput: + [name : "Tom", + age : 33, + gone : true, + otherInput: [ + name: "Ted", + age : 42] + ], + listInput : [1, 2, 3, 4, 5, 6, 7, 8] + ], + arg2: "Gone-ski" + ] + def visitor = new ValueVisitor() { + @Override + Object visitScalarValue(Object coercedValue, GraphQLScalarType inputType, ValueVisitor.InputElements inputElements) { + def fieldName = inputElements.getLastInputValueDefinition().name + if (fieldName == "age") { + return ABSENCE_SENTINEL + } + if (coercedValue == "Gone-ski") { + return ABSENCE_SENTINEL + } + if (coercedValue == 4 || coercedValue == 7) { + return ABSENCE_SENTINEL + } + return coercedValue + } + + @Override + Object visitInputObjectFieldValue(Object coercedValue, GraphQLInputObjectType inputObjectType, GraphQLInputObjectField inputObjectField, ValueVisitor.InputElements inputElements) { + def fieldName = inputElements.getLastInputValueDefinition().name + if (fieldName == "otherInput") { + return ABSENCE_SENTINEL + } + if (fieldName == "gone") { + return ABSENCE_SENTINEL + } + return coercedValue + } + } + when: + def newValues = ValueTraverser.visitPreOrder(argValues, fieldDef, visitor) + then: + newValues == [arg: + [name : "Tess", + extraInput: + [name: "Tom"], + listInput : [1, 2, 3, 5, 6, 8] + ] + ] + } + + def "can get give access to all elements and unwrapped elements"() { + def sdl = """ + + type Query { + field(arg : Input! ) : String + } + + input Input { + name : String + age : Int + objectField : [[Input!]]! + } + """ + def schema = TestUtil.schema(sdl) + + def fieldDef = schema.getObjectType("Query").getFieldDefinition("field") + def argValues = [arg: + [name : "Tess", + age : 42, + objectField: [[ + [ + name : "Tom", + age : 33, + objectField: [[ + [ + name: "Ted", + age : 42 + ] + ]] + ] + ]] + ] + ] + def captureAll = [] + def captureUnwrapped = [] + def last = "" + def visitor = new ValueVisitor() { + @Override + Object visitScalarValue(Object coercedValue, GraphQLScalarType inputType, ValueVisitor.InputElements inputElements) { + if (coercedValue == "Ted") { + captureAll = inputElements.inputElements.collect { testStr(it) } + captureUnwrapped = inputElements.unwrappedInputElements.collect { testStr(it) } + last = inputElements.lastInputValueDefinition.name + } + return coercedValue + } + + String testStr(GraphQLInputSchemaElement inputSchemaElement) { + if (inputSchemaElement instanceof GraphQLNamedSchemaElement) { + return inputSchemaElement.name + } + return inputSchemaElement.toString() + } + } + when: + def newValues = ValueTraverser.visitPreOrder(argValues, fieldDef, visitor) + then: + newValues == argValues + last == "name" + captureAll == [ + "arg", + "Input!", + "Input", + "objectField", + "[[Input!]]!", + "[[Input!]]", + "[Input!]", + "Input!", + "Input", + "objectField", + "[[Input!]]!", + "[[Input!]]", + "[Input!]", + "Input!", + "Input", + "name", + "String", + ] + captureUnwrapped == [ + "arg", + "Input", + "objectField", + "Input", + "objectField", + "Input", + "name", + "String", + ] + } + + def "can get access to directives"() { + def sdl = """ + directive @d(name : String!) on ARGUMENT_DEFINITION | INPUT_OBJECT | INPUT_FIELD_DEFINITION + + type Query { + field(arg : Input! @d(name : "argDirective") ) : String + } + + input Input { + name : String @d(name : "nameDirective") + age : Int @d(name : "ageDirective") + input : Input @d(name : "inputDirective") + } + """ + def schema = TestUtil.schema(sdl) + + def fieldDef = schema.getObjectType("Query").getFieldDefinition("field") + def argValues = [arg: + [name: "Tess", age: 42, input: + [name: "Tom", age: 33, input: + [name: "Ted", age: 42]] + ] + ] + def capture = [] + def visitor = new ValueVisitor() { + @Override + Object visitScalarValue(Object coercedValue, GraphQLScalarType inputType, ValueVisitor.InputElements inputElements) { + checkDirectives(inputElements) + return coercedValue + } + + + private void checkDirectives(ValueVisitor.InputElements inputElements) { + def lastElement = inputElements.getLastInputValueDefinition() + def directive = lastElement.getAppliedDirective("d") + if (directive != null) { + def elementNames = inputElements.getInputElements().collect( + { it -> + if (it instanceof GraphQLNamedSchemaElement) { + return it.name + } else { + it.toString() + } + }) + .join(":") + def value = directive.getArgument("name").value + capture.add(elementNames + "@" + value) + } + } + } + when: + def newValues = ValueTraverser.visitPreOrder(argValues, fieldDef, visitor) + then: + newValues == argValues + capture == [ + "arg:Input!:Input:name:String@nameDirective", + "arg:Input!:Input:age:Int@ageDirective", + + "arg:Input!:Input:input:Input:name:String@nameDirective", + "arg:Input!:Input:input:Input:age:Int@ageDirective", + + "arg:Input!:Input:input:Input:input:Input:name:String@nameDirective", + "arg:Input!:Input:input:Input:input:Input:age:Int@ageDirective" + ] + } + + def "can follow directives and change input"() { + def sdl = """ + directive @stripHtml on ARGUMENT_DEFINITION | INPUT_OBJECT | INPUT_FIELD_DEFINITION + + type Query { + field(arg : Input!) : String + } + + input Input { + name : String @stripHtml + age : Int + input : Input + } + """ + def schema = TestUtil.schema(sdl) + + def fieldDef = schema.getObjectType("Query").getFieldDefinition("field") + def argValues = [arg: + [name: "Tess", age: 42, input: + [name: "Tom", age: 33, input: + [name: "Ted", age: 42]] + ] + ] + + def visitor = new ValueVisitor() { + @Override + Object visitScalarValue(Object coercedValue, GraphQLScalarType inputType, ValueVisitor.InputElements inputElements) { + def lastElement = inputElements.getLastInputValueDefinition() + def directive = lastElement.getAppliedDirective("stripHtml") + if (directive != null) { + def v = String.valueOf(coercedValue) + return v.replaceAll(//, '').replaceAll(/<.*?>/, '') + } + return coercedValue + } + + } + when: + def newValues = ValueTraverser.visitPreOrder(argValues, fieldDef, visitor) + then: + newValues == [arg: + [name: "Tess", age: 42, input: + [name: "Tom", age: 33, input: + [name: "Ted", age: 42]] + ] + ] + } + + def "an integration test showing how to change values"() { + def sdl = """ +directive @stripHtml on ARGUMENT_DEFINITION | INPUT_FIELD_DEFINITION + +type Query { + searchProfile(contains: String! @stripHtml, limit: Int): Profile! +} + +type Mutation { + signUp(input: SignUpInput!): Profile! +} + +input SignUpInput { + username: String! @stripHtml + password: String! + firstName: String! + lastName: String! +} + +type Profile { + username: String! + fullName: String! +} +""" + def schemaDirectiveWiring = new SchemaDirectiveWiring() { + @Override + GraphQLFieldDefinition onField(SchemaDirectiveWiringEnvironment env) { + GraphQLFieldsContainer fieldsContainer = env.getFieldsContainer() + GraphQLFieldDefinition fieldDefinition = env.getFieldDefinition() + + final DataFetcher originalDF = env.getCodeRegistry().getDataFetcher(fieldsContainer, fieldDefinition) + final DataFetcher newDF = { DataFetchingEnvironment originalEnv -> + ValueVisitor visitor = new ValueVisitor() { + @Override + Object visitScalarValue(Object coercedValue, GraphQLScalarType inputType, ValueVisitor.InputElements inputElements) { + def container = inputElements.getLastInputValueDefinition() + if (container.hasAppliedDirective("stripHtml")) { + return stripHtml(coercedValue) + } + return coercedValue + } + + private String stripHtml(coercedValue) { + return String.valueOf(coercedValue) + .replaceAll(//, '') + .replaceAll(/<.*?>/, '') + } + } + DataFetchingEnvironment newEnv = ValueTraverser.visitPreOrder(originalEnv, visitor) + return originalDF.get(newEnv); + } + + env.getCodeRegistry().dataFetcher(fieldsContainer, fieldDefinition, newDF) + + return fieldDefinition + } + } + + DataFetcher searchProfileDF = { env -> + def containsArg = env.getArgument("contains") as String + return [username: containsArg] + + } + DataFetcher signUpDF = { DataFetchingEnvironment env -> + def inputArg = env.getArgument("input") as Map + def inputUserName = inputArg["username"] + return [username: inputUserName] + } + def runtimeWiring = newRuntimeWiring().directiveWiring(schemaDirectiveWiring) + .type(newTypeWiring("Query").dataFetcher("searchProfile", searchProfileDF)) + .type(newTypeWiring("Mutation").dataFetcher("signUp", signUpDF)) + .build() + def schema = TestUtil.schema(sdl, runtimeWiring) + def graphQL = GraphQL.newGraphQL(schema).build() + + def query = """ + query q { + searchProfile(contains : "someHtml") { + username + } + } + + mutation m { + signUp(input : { + username: "bbakerman" + password: "hunter2" + firstName: "Brad" + lastName: "Baker" + } + ) { + username + } + } +""" + + when: + def executionInput = ExecutionInput.newExecutionInput(query).operationName("q").build() + def er = graphQL.execute(executionInput) + then: + er.errors.isEmpty() + er.data == [searchProfile: [username: "someHtml"]] + + // mutation + when: + executionInput = ExecutionInput.newExecutionInput(query).operationName("m").build() + er = graphQL.execute(executionInput) + then: + er.errors.isEmpty() + er.data == [signUp: [username: "bbakerman"]] + } +} diff --git a/src/test/groovy/graphql/cachecontrol/CacheControlTest.groovy b/src/test/groovy/graphql/cachecontrol/CacheControlTest.groovy index c2d5e1a752..c05482e3c9 100644 --- a/src/test/groovy/graphql/cachecontrol/CacheControlTest.groovy +++ b/src/test/groovy/graphql/cachecontrol/CacheControlTest.groovy @@ -1,13 +1,26 @@ package graphql.cachecontrol import graphql.ExecutionInput -import graphql.ExecutionResultImpl +import graphql.ExecutionResult +import graphql.GraphQLContext import graphql.TestUtil +import graphql.execution.CoercedVariables +import graphql.execution.ExecutionContextBuilder +import graphql.execution.ExecutionId +import graphql.execution.ExecutionStrategy import graphql.execution.ResultPath +import graphql.execution.instrumentation.Instrumentation +import graphql.language.Document +import graphql.language.FragmentDefinition +import graphql.language.OperationDefinition +import graphql.parser.Parser import graphql.schema.DataFetcher +import graphql.schema.GraphQLSchema +import org.dataloader.DataLoaderRegistry import spock.lang.Specification class CacheControlTest extends Specification { + // All tests in this file will be deleted when CacheControl code is removed. def "can build up hints when there is no extensions present"() { def cc = CacheControl.newCacheControl() @@ -16,7 +29,7 @@ class CacheControlTest extends Specification { cc.hint(ResultPath.parse("/hint/33/private"), 33, CacheControl.Scope.PRIVATE) cc.hint(ResultPath.parse("/hint/private"), CacheControl.Scope.PRIVATE) - def er = ExecutionResultImpl.newExecutionResult().data("data").build() + def er = ExecutionResult.newExecutionResult().data("data").build() when: def newER = cc.addTo(er) @@ -43,7 +56,7 @@ class CacheControlTest extends Specification { def startingExtensions = ["someExistingExt": "data"] - def er = ExecutionResultImpl.newExecutionResult().data("data").extensions(startingExtensions).build() + def er = ExecutionResult.newExecutionResult().data("data").extensions(startingExtensions).build() when: def newER = cc.addTo(er) @@ -115,4 +128,59 @@ class CacheControlTest extends Specification { ] ] } + + def "transform works and copies values with cache control"() { + // Retain this ExecutionContext CacheControl test for coverage + given: + def cacheControl = CacheControl.newCacheControl() + def oldCoercedVariables = CoercedVariables.emptyVariables() + Instrumentation instrumentation = Mock(Instrumentation) + ExecutionStrategy queryStrategy = Mock(ExecutionStrategy) + ExecutionStrategy mutationStrategy = Mock(ExecutionStrategy) + ExecutionStrategy subscriptionStrategy = Mock(ExecutionStrategy) + GraphQLSchema schema = Mock(GraphQLSchema) + def executionId = ExecutionId.generate() + def graphQLContext = GraphQLContext.newContext().build() + def root = "root" + Document document = new Parser().parseDocument("query myQuery(\$var: String){...MyFragment} fragment MyFragment on Query{foo}") + def operation = document.definitions[0] as OperationDefinition + def fragment = document.definitions[1] as FragmentDefinition + def dataLoaderRegistry = new DataLoaderRegistry() + + def executionContextOld = new ExecutionContextBuilder() + .cacheControl(cacheControl) + .executionId(executionId) + .instrumentation(instrumentation) + .graphQLSchema(schema) + .queryStrategy(queryStrategy) + .mutationStrategy(mutationStrategy) + .subscriptionStrategy(subscriptionStrategy) + .root(root) + .graphQLContext(graphQLContext) + .coercedVariables(oldCoercedVariables) + .fragmentsByName([MyFragment: fragment]) + .operationDefinition(operation) + .dataLoaderRegistry(dataLoaderRegistry) + .build() + + when: + def coercedVariables = CoercedVariables.of([var: 'value']) + def executionContext = executionContextOld.transform(builder -> builder + .coercedVariables(coercedVariables)) + + then: + executionContext.cacheControl == cacheControl + executionContext.executionId == executionId + executionContext.instrumentation == instrumentation + executionContext.graphQLSchema == schema + executionContext.queryStrategy == queryStrategy + executionContext.mutationStrategy == mutationStrategy + executionContext.subscriptionStrategy == subscriptionStrategy + executionContext.root == root + executionContext.graphQLContext == graphQLContext + executionContext.coercedVariables == coercedVariables + executionContext.getFragmentsByName() == [MyFragment: fragment] + executionContext.operationDefinition == operation + executionContext.dataLoaderRegistry == dataLoaderRegistry + } } diff --git a/src/test/groovy/graphql/collect/ImmutableKitTest.groovy b/src/test/groovy/graphql/collect/ImmutableKitTest.groovy index e2fb0dfde7..82d76bae1e 100644 --- a/src/test/groovy/graphql/collect/ImmutableKitTest.groovy +++ b/src/test/groovy/graphql/collect/ImmutableKitTest.groovy @@ -13,14 +13,6 @@ class ImmutableKitTest extends Specification { ImmutableKit.emptyMap().size() == 0 } - def "can make an immutable map of lists"() { - when: - ImmutableMap> map = ImmutableKit.toImmutableMapOfLists([a: ["a", "A"]]) - then: - map.get("a") == ImmutableList.copyOf(["a", "A"]) - map.get("a") instanceof ImmutableList - } - def "can map a collections"() { when: def outputList = ImmutableKit.map(["quick", "brown", "fox"], { word -> word.reverse() }) diff --git a/src/test/groovy/graphql/execution/AsyncExecutionStrategyTest.groovy b/src/test/groovy/graphql/execution/AsyncExecutionStrategyTest.groovy index 29a6c4b771..951246df0c 100644 --- a/src/test/groovy/graphql/execution/AsyncExecutionStrategyTest.groovy +++ b/src/test/groovy/graphql/execution/AsyncExecutionStrategyTest.groovy @@ -2,13 +2,17 @@ package graphql.execution import graphql.ErrorType import graphql.ExecutionResult +import graphql.GraphQLContext import graphql.execution.instrumentation.ExecutionStrategyInstrumentationContext -import graphql.execution.instrumentation.SimpleInstrumentation +import graphql.execution.instrumentation.InstrumentationState +import graphql.execution.instrumentation.SimplePerformantInstrumentation import graphql.execution.instrumentation.parameters.InstrumentationExecutionStrategyParameters import graphql.language.Field import graphql.language.OperationDefinition import graphql.parser.Parser import graphql.schema.DataFetcher +import graphql.schema.FieldCoordinates +import graphql.schema.GraphQLCodeRegistry import graphql.schema.GraphQLFieldDefinition import graphql.schema.GraphQLSchema import spock.lang.Specification @@ -29,26 +33,38 @@ import static org.awaitility.Awaitility.await class AsyncExecutionStrategyTest extends Specification { GraphQLSchema schema(DataFetcher dataFetcher1, DataFetcher dataFetcher2) { - GraphQLFieldDefinition.Builder fieldDefinition = newFieldDefinition() - .name("hello") + def queryName = "RootQueryType" + def field1Name = "hello" + def field2Name = "hello2" + + GraphQLFieldDefinition.Builder fieldDefinition1 = newFieldDefinition() + .name(field1Name) .type(GraphQLString) - .dataFetcher(dataFetcher1) GraphQLFieldDefinition.Builder fieldDefinition2 = newFieldDefinition() - .name("hello2") + .name(field2Name) .type(GraphQLString) - .dataFetcher(dataFetcher2) - GraphQLSchema schema = newSchema().query( - newObject() - .name("RootQueryType") - .field(fieldDefinition) + def field1Coordinates = FieldCoordinates.coordinates(queryName, field1Name) + def field2Coordinates = FieldCoordinates.coordinates(queryName, field2Name) + + GraphQLCodeRegistry codeRegistry = GraphQLCodeRegistry.newCodeRegistry() + .dataFetcher(field1Coordinates, dataFetcher1) + .dataFetcher(field2Coordinates, dataFetcher2) + .build() + + GraphQLSchema schema = newSchema() + .codeRegistry(codeRegistry) + .query(newObject() + .name(queryName) + .field(fieldDefinition1) .field(fieldDefinition2) .build() - ).build() + ) + .build() + schema } - def "execution is serial if the dataFetchers are blocking"() { given: def lock = new ReentrantLock() @@ -80,8 +96,10 @@ class AsyncExecutionStrategyTest extends Specification { .graphQLSchema(schema) .executionId(ExecutionId.generate()) .operationDefinition(operation) - .instrumentation(SimpleInstrumentation.INSTANCE) + .instrumentation(SimplePerformantInstrumentation.INSTANCE) .valueUnboxer(ValueUnboxer.DEFAULT) + .graphQLContext(GraphQLContext.getDefault()) + .locale(Locale.getDefault()) .build() ExecutionStrategyParameters executionStrategyParameters = ExecutionStrategyParameters .newParameters() @@ -120,7 +138,9 @@ class AsyncExecutionStrategyTest extends Specification { .executionId(ExecutionId.generate()) .operationDefinition(operation) .valueUnboxer(ValueUnboxer.DEFAULT) - .instrumentation(SimpleInstrumentation.INSTANCE) + .instrumentation(SimplePerformantInstrumentation.INSTANCE) + .locale(Locale.getDefault()) + .graphQLContext(GraphQLContext.getDefault()) .build() ExecutionStrategyParameters executionStrategyParameters = ExecutionStrategyParameters .newParameters() @@ -161,7 +181,9 @@ class AsyncExecutionStrategyTest extends Specification { .executionId(ExecutionId.generate()) .operationDefinition(operation) .valueUnboxer(ValueUnboxer.DEFAULT) - .instrumentation(SimpleInstrumentation.INSTANCE) + .instrumentation(SimplePerformantInstrumentation.INSTANCE) + .graphQLContext(GraphQLContext.getDefault()) + .locale(Locale.getDefault()) .build() ExecutionStrategyParameters executionStrategyParameters = ExecutionStrategyParameters .newParameters() @@ -200,8 +222,10 @@ class AsyncExecutionStrategyTest extends Specification { .graphQLSchema(schema) .executionId(ExecutionId.generate()) .operationDefinition(operation) - .instrumentation(SimpleInstrumentation.INSTANCE) + .instrumentation(SimplePerformantInstrumentation.INSTANCE) .valueUnboxer(ValueUnboxer.DEFAULT) + .locale(Locale.getDefault()) + .graphQLContext(GraphQLContext.getDefault()) .build() ExecutionStrategyParameters executionStrategyParameters = ExecutionStrategyParameters .newParameters() @@ -240,9 +264,12 @@ class AsyncExecutionStrategyTest extends Specification { .executionId(ExecutionId.generate()) .operationDefinition(operation) .valueUnboxer(ValueUnboxer.DEFAULT) - .instrumentation(new SimpleInstrumentation() { + .graphQLContext(GraphQLContext.getDefault()) + .locale(Locale.getDefault()) + .instrumentation(new SimplePerformantInstrumentation() { + @Override - ExecutionStrategyInstrumentationContext beginExecutionStrategy(InstrumentationExecutionStrategyParameters parameters) { + ExecutionStrategyInstrumentationContext beginExecutionStrategy(InstrumentationExecutionStrategyParameters parameters, InstrumentationState state) { return new ExecutionStrategyInstrumentationContext() { @Override @@ -251,13 +278,11 @@ class AsyncExecutionStrategyTest extends Specification { } @Override - public void onDispatched(CompletableFuture result) { - + void onDispatched(CompletableFuture result) { } @Override - public void onCompleted(ExecutionResult result, Throwable t) { - + void onCompleted(ExecutionResult result, Throwable t) { } } } diff --git a/src/test/groovy/graphql/execution/AsyncSerialExecutionStrategyTest.groovy b/src/test/groovy/graphql/execution/AsyncSerialExecutionStrategyTest.groovy index 7730f51b98..1d811bd3bd 100644 --- a/src/test/groovy/graphql/execution/AsyncSerialExecutionStrategyTest.groovy +++ b/src/test/groovy/graphql/execution/AsyncSerialExecutionStrategyTest.groovy @@ -1,13 +1,16 @@ package graphql.execution - -import graphql.execution.instrumentation.SimpleInstrumentation +import graphql.GraphQLContext +import graphql.execution.instrumentation.SimplePerformantInstrumentation import graphql.language.Field import graphql.language.OperationDefinition import graphql.parser.Parser import graphql.schema.DataFetcher +import graphql.schema.FieldCoordinates +import graphql.schema.GraphQLCodeRegistry import graphql.schema.GraphQLFieldDefinition import graphql.schema.GraphQLSchema +import graphql.schema.LightDataFetcher import spock.lang.Specification import java.util.concurrent.CompletableFuture @@ -23,27 +26,42 @@ import static graphql.schema.GraphQLSchema.newSchema class AsyncSerialExecutionStrategyTest extends Specification { GraphQLSchema schema(DataFetcher dataFetcher1, DataFetcher dataFetcher2, DataFetcher dataFetcher3) { - GraphQLFieldDefinition.Builder fieldDefinition = newFieldDefinition() - .name("hello") + def queryName = "RootQueryType" + def field1Name = "hello" + def field2Name = "hello2" + def field3Name = "hello3" + + GraphQLFieldDefinition.Builder fieldDefinition1 = newFieldDefinition() + .name(field1Name) .type(GraphQLString) - .dataFetcher(dataFetcher1) GraphQLFieldDefinition.Builder fieldDefinition2 = newFieldDefinition() - .name("hello2") + .name(field2Name) .type(GraphQLString) - .dataFetcher(dataFetcher2) GraphQLFieldDefinition.Builder fieldDefinition3 = newFieldDefinition() - .name("hello3") + .name(field3Name) .type(GraphQLString) - .dataFetcher(dataFetcher3) - GraphQLSchema schema = newSchema().query( - newObject() - .name("RootQueryType") - .field(fieldDefinition) + def field1Coordinates = FieldCoordinates.coordinates(queryName, field1Name) + def field2Coordinates = FieldCoordinates.coordinates(queryName, field2Name) + def field3Coordinates = FieldCoordinates.coordinates(queryName, field3Name) + + GraphQLCodeRegistry codeRegistry = GraphQLCodeRegistry.newCodeRegistry() + .dataFetcher(field1Coordinates, dataFetcher1) + .dataFetcher(field2Coordinates, dataFetcher2) + .dataFetcher(field3Coordinates, dataFetcher3) + .build() + + GraphQLSchema schema = newSchema() + .codeRegistry(codeRegistry) + .query(newObject() + .name(queryName) + .field(fieldDefinition1) .field(fieldDefinition2) .field(fieldDefinition3) .build() - ).build() + ) + .build() + schema } @@ -84,8 +102,10 @@ class AsyncSerialExecutionStrategyTest extends Specification { .graphQLSchema(schema) .executionId(ExecutionId.generate()) .operationDefinition(operation) - .instrumentation(SimpleInstrumentation.INSTANCE) + .instrumentation(SimplePerformantInstrumentation.INSTANCE) .valueUnboxer(ValueUnboxer.DEFAULT) + .locale(Locale.getDefault()) + .graphQLContext(GraphQLContext.getDefault()) .build() ExecutionStrategyParameters executionStrategyParameters = ExecutionStrategyParameters .newParameters() @@ -106,13 +126,13 @@ class AsyncSerialExecutionStrategyTest extends Specification { @SuppressWarnings("GroovyAssignabilityCheck") def "async serial execution test"() { given: - def df1 = Mock(DataFetcher) + def df1 = Mock(LightDataFetcher) def cf1 = new CompletableFuture() - def df2 = Mock(DataFetcher) + def df2 = Mock(LightDataFetcher) def cf2 = new CompletableFuture() - def df3 = Mock(DataFetcher) + def df3 = Mock(LightDataFetcher) def cf3 = new CompletableFuture() GraphQLSchema schema = schema(df1, df2, df3) @@ -128,8 +148,10 @@ class AsyncSerialExecutionStrategyTest extends Specification { .graphQLSchema(schema) .executionId(ExecutionId.generate()) .operationDefinition(operation) - .instrumentation(SimpleInstrumentation.INSTANCE) + .instrumentation(SimplePerformantInstrumentation.INSTANCE) .valueUnboxer(ValueUnboxer.DEFAULT) + .locale(Locale.getDefault()) + .graphQLContext(GraphQLContext.getDefault()) .build() ExecutionStrategyParameters executionStrategyParameters = ExecutionStrategyParameters .newParameters() @@ -144,35 +166,35 @@ class AsyncSerialExecutionStrategyTest extends Specification { then: !result.isDone() - 1 * df1.get(_) >> cf1 - 0 * df2.get(_) >> cf2 - 0 * df3.get(_) >> cf3 + 1 * df1.get(_,_,_) >> cf1 + 0 * df2.get(_,_,_) >> cf2 + 0 * df3.get(_,_,_) >> cf3 when: cf1.complete("world1") then: !result.isDone() - 0 * df1.get(_) >> cf1 - 1 * df2.get(_) >> cf2 - 0 * df3.get(_) >> cf3 + 0 * df1.get(_,_,_) >> cf1 + 1 * df2.get(_,_,_) >> cf2 + 0 * df3.get(_,_,_) >> cf3 when: cf2.complete("world2") then: !result.isDone() - 0 * df1.get(_) >> cf1 - 0 * df2.get(_) >> cf2 - 1 * df3.get(_) >> cf3 + 0 * df1.get(_,_,_) >> cf1 + 0 * df2.get(_,_,_) >> cf2 + 1 * df3.get(_,_,_) >> cf3 when: cf3.complete("world3") then: - 0 * df1.get(_) >> cf1 - 0 * df2.get(_) >> cf2 - 0 * df3.get(_) >> cf3 + 0 * df1.get(_,_,_) >> cf1 + 0 * df2.get(_,_,_) >> cf2 + 0 * df3.get(_,_,_) >> cf3 result.isDone() result.get().data == ['hello': 'world1', 'hello2': 'world2', 'hello3': 'world3'] } diff --git a/src/test/groovy/graphql/execution/AsyncTest.groovy b/src/test/groovy/graphql/execution/AsyncTest.groovy index 1ac9650ef8..c79f483b6d 100644 --- a/src/test/groovy/graphql/execution/AsyncTest.groovy +++ b/src/test/groovy/graphql/execution/AsyncTest.groovy @@ -130,15 +130,4 @@ class AsyncTest extends Specification { exception.getCause().getMessage() == "some error" } - def "each works for list of futures "() { - given: - completedFuture('x') - - when: - def result = Async.each([completedFuture('x'), completedFuture('y'), completedFuture('z')]) - - then: - result.isDone() - result.get() == ['x', 'y', 'z'] - } } diff --git a/src/test/groovy/graphql/execution/DataFetcherExceptionHandlerTest.groovy b/src/test/groovy/graphql/execution/DataFetcherExceptionHandlerTest.groovy index 8a711ab216..0587dc4e8a 100644 --- a/src/test/groovy/graphql/execution/DataFetcherExceptionHandlerTest.groovy +++ b/src/test/groovy/graphql/execution/DataFetcherExceptionHandlerTest.groovy @@ -76,11 +76,6 @@ class DataFetcherExceptionHandlerTest extends Specification { def "integration test to prove an async custom error handle can be made"() { DataFetcherExceptionHandler handler = new DataFetcherExceptionHandler() { - @Override - DataFetcherExceptionHandlerResult onException(DataFetcherExceptionHandlerParameters handlerParameters) { - return null - } - @Override CompletableFuture handleException(DataFetcherExceptionHandlerParameters params) { def msg = "The thing went " + params.getException().getMessage() @@ -117,11 +112,6 @@ class DataFetcherExceptionHandlerTest extends Specification { def "if an async exception handler itself throws an exception than that is handled"() { DataFetcherExceptionHandler handler = new DataFetcherExceptionHandler() { - @Override - DataFetcherExceptionHandlerResult onException(DataFetcherExceptionHandlerParameters handlerParameters) { - return null - } - @Override CompletableFuture handleException(DataFetcherExceptionHandlerParameters handlerParameters) { throw new RuntimeException("The handler itself went BANG!") @@ -140,11 +130,6 @@ class DataFetcherExceptionHandlerTest extends Specification { def "multiple errors can be returned in a handler"() { DataFetcherExceptionHandler handler = new DataFetcherExceptionHandler() { - @Override - DataFetcherExceptionHandlerResult onException(DataFetcherExceptionHandlerParameters handlerParameters) { - return null - } - @Override CompletableFuture handleException(DataFetcherExceptionHandlerParameters params) { def result = DataFetcherExceptionHandlerResult.newResult() diff --git a/src/test/groovy/graphql/execution/ExecutionContextBuilderTest.groovy b/src/test/groovy/graphql/execution/ExecutionContextBuilderTest.groovy index 861743f5c3..eb35f277b7 100644 --- a/src/test/groovy/graphql/execution/ExecutionContextBuilderTest.groovy +++ b/src/test/groovy/graphql/execution/ExecutionContextBuilderTest.groovy @@ -1,7 +1,6 @@ package graphql.execution import graphql.GraphQLContext -import graphql.cachecontrol.CacheControl import graphql.execution.instrumentation.Instrumentation import graphql.language.Document import graphql.language.FragmentDefinition @@ -26,7 +25,6 @@ class ExecutionContextBuilderTest extends Specification { def operation = document.definitions[0] as OperationDefinition def fragment = document.definitions[1] as FragmentDefinition def dataLoaderRegistry = new DataLoaderRegistry() - def cacheControl = CacheControl.newCacheControl() def "builds the correct ExecutionContext"() { when: @@ -37,14 +35,13 @@ class ExecutionContextBuilderTest extends Specification { .subscriptionStrategy(subscriptionStrategy) .graphQLSchema(schema) .executionId(executionId) - .context(context) + .context(context) // Retain deprecated builder for test coverage .graphQLContext(graphQLContext) .root(root) .operationDefinition(operation) .fragmentsByName([MyFragment: fragment]) - .variables([var: 'value']) + .variables([var: 'value']) // Retain deprecated builder for test coverage .dataLoaderRegistry(dataLoaderRegistry) - .cacheControl(cacheControl) .build() then: @@ -55,13 +52,12 @@ class ExecutionContextBuilderTest extends Specification { executionContext.mutationStrategy == mutationStrategy executionContext.subscriptionStrategy == subscriptionStrategy executionContext.root == root - executionContext.context == context + executionContext.context == context // Retain deprecated method for test coverage executionContext.graphQLContext == graphQLContext - executionContext.variables == [var: 'value'] + executionContext.variables == [var: 'value'] // Retain deprecated method for test coverage executionContext.getFragmentsByName() == [MyFragment: fragment] executionContext.operationDefinition == operation executionContext.dataLoaderRegistry == dataLoaderRegistry - executionContext.cacheControl == cacheControl } def "builds the correct ExecutionContext with coerced variables"() { @@ -76,14 +72,12 @@ class ExecutionContextBuilderTest extends Specification { .subscriptionStrategy(subscriptionStrategy) .graphQLSchema(schema) .executionId(executionId) - .context(context) .graphQLContext(graphQLContext) .root(root) .operationDefinition(operation) .fragmentsByName([MyFragment: fragment]) .coercedVariables(coercedVariables) .dataLoaderRegistry(dataLoaderRegistry) - .cacheControl(cacheControl) .build() then: @@ -94,13 +88,11 @@ class ExecutionContextBuilderTest extends Specification { executionContext.mutationStrategy == mutationStrategy executionContext.subscriptionStrategy == subscriptionStrategy executionContext.root == root - executionContext.context == context executionContext.graphQLContext == graphQLContext executionContext.coercedVariables == coercedVariables executionContext.getFragmentsByName() == [MyFragment: fragment] executionContext.operationDefinition == operation executionContext.dataLoaderRegistry == dataLoaderRegistry - executionContext.cacheControl == cacheControl } def "builds the correct ExecutionContext, if both variables and coercedVariables are set, latest value set takes precedence"() { @@ -115,15 +107,12 @@ class ExecutionContextBuilderTest extends Specification { .subscriptionStrategy(subscriptionStrategy) .graphQLSchema(schema) .executionId(executionId) - .context(context) .graphQLContext(graphQLContext) .root(root) .operationDefinition(operation) .fragmentsByName([MyFragment: fragment]) - .variables([var: 'value']) .coercedVariables(coercedVariables) .dataLoaderRegistry(dataLoaderRegistry) - .cacheControl(cacheControl) .build() then: @@ -134,13 +123,11 @@ class ExecutionContextBuilderTest extends Specification { executionContext.mutationStrategy == mutationStrategy executionContext.subscriptionStrategy == subscriptionStrategy executionContext.root == root - executionContext.context == context executionContext.graphQLContext == graphQLContext executionContext.coercedVariables == coercedVariables executionContext.getFragmentsByName() == [MyFragment: fragment] executionContext.operationDefinition == operation executionContext.dataLoaderRegistry == dataLoaderRegistry - executionContext.cacheControl == cacheControl } def "transform works and copies values with coerced variables"() { @@ -153,14 +140,12 @@ class ExecutionContextBuilderTest extends Specification { .subscriptionStrategy(subscriptionStrategy) .graphQLSchema(schema) .executionId(executionId) - .context(context) .graphQLContext(graphQLContext) .root(root) .operationDefinition(operation) .coercedVariables(oldCoercedVariables) .fragmentsByName([MyFragment: fragment]) .dataLoaderRegistry(dataLoaderRegistry) - .cacheControl(cacheControl) .build() when: @@ -176,13 +161,11 @@ class ExecutionContextBuilderTest extends Specification { executionContext.mutationStrategy == mutationStrategy executionContext.subscriptionStrategy == subscriptionStrategy executionContext.root == root - executionContext.context == context executionContext.graphQLContext == graphQLContext executionContext.coercedVariables == coercedVariables executionContext.getFragmentsByName() == [MyFragment: fragment] executionContext.operationDefinition == operation executionContext.dataLoaderRegistry == dataLoaderRegistry - executionContext.cacheControl == cacheControl } def "transform copies values, if both variables and coercedVariables set, latest value set takes precedence"() { @@ -195,21 +178,17 @@ class ExecutionContextBuilderTest extends Specification { .subscriptionStrategy(subscriptionStrategy) .graphQLSchema(schema) .executionId(executionId) - .context(context) .graphQLContext(graphQLContext) .root(root) .operationDefinition(operation) - .variables([:]) .coercedVariables(oldCoercedVariables) .fragmentsByName([MyFragment: fragment]) .dataLoaderRegistry(dataLoaderRegistry) - .cacheControl(cacheControl) .build() when: def coercedVariables = CoercedVariables.of([var: 'value']) def executionContext = executionContextOld.transform(builder -> builder - .variables([var: 'value']) .coercedVariables(coercedVariables)) then: @@ -220,12 +199,10 @@ class ExecutionContextBuilderTest extends Specification { executionContext.mutationStrategy == mutationStrategy executionContext.subscriptionStrategy == subscriptionStrategy executionContext.root == root - executionContext.context == context executionContext.graphQLContext == graphQLContext executionContext.coercedVariables == coercedVariables executionContext.getFragmentsByName() == [MyFragment: fragment] executionContext.operationDefinition == operation executionContext.dataLoaderRegistry == dataLoaderRegistry - executionContext.cacheControl == cacheControl } } diff --git a/src/test/groovy/graphql/execution/ExecutionStepInfoTest.groovy b/src/test/groovy/graphql/execution/ExecutionStepInfoTest.groovy index e3a7f7a67d..9fc34114a5 100644 --- a/src/test/groovy/graphql/execution/ExecutionStepInfoTest.groovy +++ b/src/test/groovy/graphql/execution/ExecutionStepInfoTest.groovy @@ -37,7 +37,6 @@ class ExecutionStepInfoTest extends Specification { def interfaceType = GraphQLInterfaceType.newInterface().name("Interface") .field(field1Def) - .typeResolver({ env -> null }) .build() def fieldType = GraphQLObjectType.newObject() @@ -50,7 +49,6 @@ class ExecutionStepInfoTest extends Specification { .field(newFieldDefinition().name("rootField1").type(fieldType)) .build() - def "basic hierarchy"() { given: def rootTypeInfo = newExecutionStepInfo().type(rootType).build() diff --git a/src/test/groovy/graphql/execution/ExecutionStrategyExceptionHandlingEquivalenceTest.groovy b/src/test/groovy/graphql/execution/ExecutionStrategyExceptionHandlingEquivalenceTest.groovy index 7a47cee7ad..af179821cb 100644 --- a/src/test/groovy/graphql/execution/ExecutionStrategyExceptionHandlingEquivalenceTest.groovy +++ b/src/test/groovy/graphql/execution/ExecutionStrategyExceptionHandlingEquivalenceTest.groovy @@ -4,7 +4,8 @@ import graphql.ExecutionInput import graphql.GraphQL import graphql.StarWarsSchema import graphql.execution.instrumentation.InstrumentationContext -import graphql.execution.instrumentation.SimpleInstrumentation +import graphql.execution.instrumentation.InstrumentationState +import graphql.execution.instrumentation.SimplePerformantInstrumentation import graphql.execution.instrumentation.parameters.InstrumentationFieldFetchParameters import graphql.validation.ValidationError import graphql.validation.ValidationErrorType @@ -13,11 +14,11 @@ import spock.lang.Unroll class ExecutionStrategyExceptionHandlingEquivalenceTest extends Specification { - class TestInstrumentation extends SimpleInstrumentation { + class TestInstrumentation extends SimplePerformantInstrumentation { @Override - InstrumentationContext beginFieldFetch(InstrumentationFieldFetchParameters parameters) { - throw new AbortExecutionException([new ValidationError(ValidationErrorType.UnknownType)]) + InstrumentationContext beginFieldFetch(InstrumentationFieldFetchParameters parameters, InstrumentationState state) { + throw new AbortExecutionException([new ValidationError(ValidationErrorType.UnknownType)]) // Retain as there is no alternative constructor for ValidationError } } diff --git a/src/test/groovy/graphql/execution/ExecutionStrategyTest.groovy b/src/test/groovy/graphql/execution/ExecutionStrategyTest.groovy index 7a480b4f8e..412ff3fc65 100644 --- a/src/test/groovy/graphql/execution/ExecutionStrategyTest.groovy +++ b/src/test/groovy/graphql/execution/ExecutionStrategyTest.groovy @@ -10,7 +10,8 @@ import graphql.SerializationError import graphql.StarWarsSchema import graphql.TypeMismatchError import graphql.execution.instrumentation.InstrumentationContext -import graphql.execution.instrumentation.SimpleInstrumentation +import graphql.execution.instrumentation.InstrumentationState +import graphql.execution.instrumentation.SimplePerformantInstrumentation import graphql.execution.instrumentation.parameters.InstrumentationFieldCompleteParameters import graphql.language.Argument import graphql.language.Field @@ -21,15 +22,19 @@ import graphql.parser.Parser import graphql.schema.Coercing import graphql.schema.DataFetcher import graphql.schema.DataFetchingEnvironment +import graphql.schema.FieldCoordinates +import graphql.schema.GraphQLCodeRegistry import graphql.schema.GraphQLEnumType import graphql.schema.GraphQLFieldDefinition import graphql.schema.GraphQLScalarType import graphql.schema.GraphQLSchema +import graphql.schema.LightDataFetcher import org.dataloader.DataLoaderRegistry import spock.lang.Specification import java.util.concurrent.CompletableFuture import java.util.concurrent.CompletionException +import java.util.function.Supplier import java.util.stream.Stream import static ExecutionStrategyParameters.newParameters @@ -65,15 +70,14 @@ class ExecutionStrategyTest extends Specification { ExecutionId executionId = ExecutionId.from("executionId123") def variables = [arg1: "value1"] def builder = ExecutionContextBuilder.newExecutionContextBuilder() - .instrumentation(SimpleInstrumentation.INSTANCE) + .instrumentation(SimplePerformantInstrumentation.INSTANCE) .executionId(executionId) .graphQLSchema(schema ?: StarWarsSchema.starWarsSchema) .queryStrategy(executionStrategy) .mutationStrategy(executionStrategy) .subscriptionStrategy(executionStrategy) - .variables(variables) - .context("context") - .graphQLContext(GraphQLContext.newContext().of("key","context").build()) + .coercedVariables(CoercedVariables.of(variables)) + .graphQLContext(GraphQLContext.newContext().of("key", "context").build()) .root("root") .dataLoaderRegistry(new DataLoaderRegistry()) .locale(Locale.getDefault()) @@ -86,20 +90,32 @@ class ExecutionStrategyTest extends Specification { def "complete values always calls query strategy to execute more"() { given: def dataFetcher = Mock(DataFetcher) + + def someFieldName = "someField" + def testTypeName = "Test" def fieldDefinition = newFieldDefinition() - .name("someField") + .name(someFieldName) .type(GraphQLString) - .dataFetcher(dataFetcher) .build() def objectType = newObject() - .name("Test") + .name(testTypeName) .field(fieldDefinition) .build() + def someFieldCoordinates = FieldCoordinates.coordinates(testTypeName, someFieldName) + + GraphQLCodeRegistry codeRegistry = GraphQLCodeRegistry.newCodeRegistry() + .dataFetcher(someFieldCoordinates, dataFetcher) + .build() + def document = new Parser().parseDocument("{someField}") def operation = document.definitions[0] as OperationDefinition - GraphQLSchema schema = GraphQLSchema.newSchema().query(objectType).build() + GraphQLSchema schema = GraphQLSchema.newSchema() + .codeRegistry(codeRegistry) + .query(objectType) + .build() + def builder = new ExecutionContextBuilder() builder.queryStrategy(Mock(ExecutionStrategy)) builder.mutationStrategy(Mock(ExecutionStrategy)) @@ -441,7 +457,7 @@ class ExecutionStrategyTest extends Specification { throw new UnsupportedOperationException("Not implemented") } }) - .build() + .build() ExecutionContext executionContext = buildContext() @@ -483,31 +499,42 @@ class ExecutionStrategyTest extends Specification { @SuppressWarnings("GroovyVariableNotAssigned") def "resolveField creates correct DataFetchingEnvironment"() { - def dataFetcher = Mock(DataFetcher) + def dataFetcher = Mock(LightDataFetcher) + def someFieldName = "someField" + def testTypeName = "Type" def fieldDefinition = newFieldDefinition() - .name("someField") + .name(someFieldName) .type(GraphQLString) - .dataFetcher(dataFetcher) .argument(newArgument().name("arg1").type(GraphQLString)) .build() def objectType = newObject() - .name("Test") + .name(testTypeName) .field(fieldDefinition) .build() - GraphQLSchema schema = GraphQLSchema.newSchema().query(objectType).build() + def someFieldCoordinates = FieldCoordinates.coordinates(testTypeName, someFieldName) + + GraphQLCodeRegistry codeRegistry = GraphQLCodeRegistry.newCodeRegistry() + .dataFetcher(someFieldCoordinates, dataFetcher) + .build() + + GraphQLSchema schema = GraphQLSchema.newSchema() + .codeRegistry(codeRegistry) + .query(objectType) + .build() ExecutionContext executionContext = buildContext(schema) ExecutionStepInfo typeInfo = ExecutionStepInfo.newExecutionStepInfo().type(objectType).build() NonNullableFieldValidator nullableFieldValidator = new NonNullableFieldValidator(executionContext, typeInfo) Argument argument = new Argument("arg1", new StringValue("argVal")) - Field field = new Field("someField", [argument]) + Field field = new Field(someFieldName, [argument]) + MergedField mergedField = mergedField(field) ResultPath resultPath = ResultPath.rootPath().segment("test") def parameters = newParameters() .executionStepInfo(typeInfo) .source("source") .fields(mergedSelectionSet(["someField": [field]])) - .field(mergedField(field)) + .field(mergedField) .nonNullFieldValidator(nullableFieldValidator) .path(resultPath) .build() @@ -517,13 +544,12 @@ class ExecutionStrategyTest extends Specification { executionStrategy.resolveField(executionContext, parameters) then: - 1 * dataFetcher.get(_) >> { args -> environment = args[0] } + 1 * dataFetcher.get(_,_,_) >> { environment = (it[2] as Supplier).get() } environment.fieldDefinition == fieldDefinition environment.graphQLSchema == schema - environment.context == "context" environment.graphQlContext.get("key") == "context" environment.source == "source" - environment.fields == [field] + environment.mergedField == mergedField environment.root == "root" environment.parentType == objectType environment.arguments == ["arg1": "argVal"] @@ -540,19 +566,34 @@ class ExecutionStrategyTest extends Specification { throw expectedException } } - def fieldDefinition = newFieldDefinition().name("someField").type(GraphQLString).dataFetcher(dataFetcher).build() + + def someFieldName = "someField" + def testTypeName = "Test" + def fieldDefinition = newFieldDefinition() + .name(someFieldName) + .type(GraphQLString) + .build() def objectType = newObject() - .name("Test") + .name(testTypeName) .field(fieldDefinition) .build() - def schema = GraphQLSchema.newSchema().query(objectType).build() + + def someFieldCoordinates = FieldCoordinates.coordinates(testTypeName, someFieldName) + + GraphQLCodeRegistry codeRegistry = GraphQLCodeRegistry.newCodeRegistry() + .dataFetcher(someFieldCoordinates, dataFetcher) + .build() + def schema = GraphQLSchema.newSchema() + .codeRegistry(codeRegistry) + .query(objectType) + .build() ExecutionContext executionContext = buildContext(schema) def typeInfo = ExecutionStepInfo.newExecutionStepInfo().type(objectType).build() NonNullableFieldValidator nullableFieldValidator = new NonNullableFieldValidator(executionContext, typeInfo) - ResultPath expectedPath = ResultPath.rootPath().segment("someField") + ResultPath expectedPath = ResultPath.rootPath().segment(someFieldName) SourceLocation sourceLocation = new SourceLocation(666, 999) - Field field = Field.newField("someField").sourceLocation(sourceLocation).build() + Field field = Field.newField(someFieldName).sourceLocation(sourceLocation).build() def parameters = newParameters() .executionStepInfo(typeInfo) .source("source") @@ -564,7 +605,6 @@ class ExecutionStrategyTest extends Specification { [executionContext, fieldDefinition, expectedPath, parameters, field, sourceLocation] } - def "test that the new data fetcher error handler interface is called"() { def expectedException = new UnsupportedOperationException("This is the exception you are looking for") @@ -637,16 +677,16 @@ class ExecutionStrategyTest extends Specification { def (ExecutionContext executionContext, GraphQLFieldDefinition fieldDefinition, ResultPath expectedPath, ExecutionStrategyParameters params, Field field, SourceLocation sourceLocation) = exceptionSetupFixture(expectedException) ExecutionContextBuilder executionContextBuilder = ExecutionContextBuilder.newExecutionContextBuilder(executionContext) - def instrumentation = new SimpleInstrumentation() { + def instrumentation = new SimplePerformantInstrumentation() { Map fetchedValues = [:] @Override - InstrumentationContext beginFieldComplete(InstrumentationFieldCompleteParameters parameters) { + InstrumentationContext beginFieldComplete(InstrumentationFieldCompleteParameters parameters, InstrumentationState state) { if (parameters.fetchedValue instanceof FetchedValue) { FetchedValue value = (FetchedValue) parameters.fetchedValue fetchedValues.put(parameters.field.name, value) } - return super.beginFieldComplete(parameters) + return super.beginFieldComplete(parameters, state) } } ExecutionContext instrumentedExecutionContext = executionContextBuilder.instrumentation(instrumentation).build() @@ -681,22 +721,34 @@ class ExecutionStrategyTest extends Specification { throw new RuntimeException("bang") } } + + def someFieldName = "someField" + def testTypeName = "Test" + def fieldDefinition = newFieldDefinition() - .name("someField") + .name(someFieldName) .type(nonNull(GraphQLString)) - .dataFetcher(dataFetcher) .build() def objectType = newObject() - .name("Test") + .name(testTypeName) .field(fieldDefinition) .build() - GraphQLSchema schema = GraphQLSchema.newSchema().query(objectType).build() + def someFieldCoordinates = FieldCoordinates.coordinates(testTypeName, someFieldName) + + GraphQLCodeRegistry codeRegistry = GraphQLCodeRegistry.newCodeRegistry() + .dataFetcher(someFieldCoordinates, dataFetcher) + .build() + + GraphQLSchema schema = GraphQLSchema.newSchema() + .codeRegistry(codeRegistry) + .query(objectType) + .build() ExecutionContext executionContext = buildContext(schema) def typeInfo = ExecutionStepInfo.newExecutionStepInfo().type(objectType).build() NonNullableFieldValidator nullableFieldValidator = new NonNullableFieldValidator(executionContext, typeInfo) - Field field = new Field("someField") + Field field = new Field(someFieldName) def parameters = newParameters() .executionStepInfo(typeInfo) @@ -743,7 +795,7 @@ class ExecutionStrategyTest extends Specification { def "#842 completes value for java.util.Stream"() { given: ExecutionContext executionContext = buildContext() - Stream result = Stream.of(1, 2, 3) + Stream result = Stream.of(1L, 2L, 3L) def fieldType = list(Scalars.GraphQLInt) def fldDef = newFieldDefinition().name("test").type(fieldType).build() def executionStepInfo = ExecutionStepInfo.newExecutionStepInfo().type(fieldType).path(ResultPath.rootPath()).fieldDefinition(fldDef).build() @@ -816,7 +868,7 @@ class ExecutionStrategyTest extends Specification { fetchedValue.getFetchedValue() == executionData // executionContext.getErrors()[0].locations == [new SourceLocation(7, 20)] executionContext.getErrors()[0].message == "bad foo" - executionContext.getErrors()[0].path == [ "child", "foo"] + executionContext.getErrors()[0].path == ["child", "foo"] } def "#1558 forward localContext on nonBoxed return from DataFetcher"() { diff --git a/src/test/groovy/graphql/execution/ExecutionTest.groovy b/src/test/groovy/graphql/execution/ExecutionTest.groovy index c2539ef6c6..0cc8f1d287 100644 --- a/src/test/groovy/graphql/execution/ExecutionTest.groovy +++ b/src/test/groovy/graphql/execution/ExecutionTest.groovy @@ -5,9 +5,10 @@ import graphql.ExecutionResult import graphql.ExecutionResultImpl import graphql.MutationSchema import graphql.execution.instrumentation.InstrumentationState -import graphql.execution.instrumentation.SimpleInstrumentation +import graphql.execution.instrumentation.SimplePerformantInstrumentation import graphql.execution.instrumentation.parameters.InstrumentationExecutionParameters import graphql.parser.Parser +import org.jetbrains.annotations.NotNull import spock.lang.Specification import java.util.concurrent.CompletableFuture @@ -35,7 +36,7 @@ class ExecutionTest extends Specification { def subscriptionStrategy = new CountingExecutionStrategy() def mutationStrategy = new CountingExecutionStrategy() def queryStrategy = new CountingExecutionStrategy() - def execution = new Execution(queryStrategy, mutationStrategy, subscriptionStrategy, SimpleInstrumentation.INSTANCE, ValueUnboxer.DEFAULT) + def execution = new Execution(queryStrategy, mutationStrategy, subscriptionStrategy, SimplePerformantInstrumentation.INSTANCE, ValueUnboxer.DEFAULT) def emptyExecutionInput = ExecutionInput.newExecutionInput().query("query").build() def instrumentationState = new InstrumentationState() {} @@ -98,24 +99,25 @@ class ExecutionTest extends Specification { mutationStrategy.execute == 0 subscriptionStrategy.execute == 1 } - - def "Update query strategy when instrumenting execution context" (){ - given: - def query = ''' + + def "Update query strategy when instrumenting execution context"() { + given: + def query = ''' query { numberHolder { theNumber } } ''' - def document = parser.parseDocument(query) - def queryStrategyUpdatedToDuringExecutionContextInstrument = new CountingExecutionStrategy() - - def instrumentation = new SimpleInstrumentation() { + def document = parser.parseDocument(query) + def queryStrategyUpdatedToDuringExecutionContextInstrument = new CountingExecutionStrategy() + + def instrumentation = new SimplePerformantInstrumentation() { @Override ExecutionContext instrumentExecutionContext(ExecutionContext executionContext, - InstrumentationExecutionParameters parameters) { + InstrumentationExecutionParameters parameters, + InstrumentationState state) { return ExecutionContextBuilder.newExecutionContextBuilder(executionContext) .queryStrategy(queryStrategyUpdatedToDuringExecutionContextInstrument) @@ -124,17 +126,17 @@ class ExecutionTest extends Specification { } def execution = new Execution(queryStrategy, mutationStrategy, subscriptionStrategy, instrumentation, ValueUnboxer.DEFAULT) - - - when: - execution.execute(document, MutationSchema.schema, ExecutionId.generate(), emptyExecutionInput, instrumentationState) - - then: - queryStrategy.execute == 0 - mutationStrategy.execute == 0 - subscriptionStrategy.execute == 0 - queryStrategyUpdatedToDuringExecutionContextInstrument.execute == 1 - } - - + + + when: + execution.execute(document, MutationSchema.schema, ExecutionId.generate(), emptyExecutionInput, instrumentationState) + + then: + queryStrategy.execute == 0 + mutationStrategy.execute == 0 + subscriptionStrategy.execute == 0 + queryStrategyUpdatedToDuringExecutionContextInstrument.execute == 1 + } + + } diff --git a/src/test/groovy/graphql/execution/SimpleDataFetcherExceptionHandlerTest.groovy b/src/test/groovy/graphql/execution/SimpleDataFetcherExceptionHandlerTest.groovy index eac8b6f44b..1980db0816 100644 --- a/src/test/groovy/graphql/execution/SimpleDataFetcherExceptionHandlerTest.groovy +++ b/src/test/groovy/graphql/execution/SimpleDataFetcherExceptionHandlerTest.groovy @@ -52,7 +52,7 @@ class SimpleDataFetcherExceptionHandlerTest extends Specification { when: DataFetcherExceptionHandler handler = new MyHandler() def handlerParameters = mkParams(new RuntimeException("RTE")) - def result = handler.onException(handlerParameters) + def result = handler.onException(handlerParameters) // Retain deprecated method for test coverage then: result.errors[0] instanceof ExceptionWhileDataFetching diff --git a/src/test/groovy/graphql/execution/ValuesResolverTest.groovy b/src/test/groovy/graphql/execution/ValuesResolverTest.groovy index 2a1a642c5b..9c2ec5d2d0 100644 --- a/src/test/groovy/graphql/execution/ValuesResolverTest.groovy +++ b/src/test/groovy/graphql/execution/ValuesResolverTest.groovy @@ -2,6 +2,7 @@ package graphql.execution import graphql.ErrorType import graphql.ExecutionInput +import graphql.GraphQLContext import graphql.GraphQLException import graphql.TestUtil import graphql.language.Argument @@ -37,13 +38,17 @@ import static graphql.schema.GraphQLNonNull.nonNull class ValuesResolverTest extends Specification { + def graphQLContext = GraphQLContext.getDefault() + def locale = Locale.getDefault() + + @Unroll def "getVariableValues: simple variable input #inputValue"() { given: def schema = TestUtil.schemaWithInputType(inputType) VariableDefinition variableDefinition = new VariableDefinition("variable", variableType, null) when: - def resolvedValues = ValuesResolver.coerceVariableValues(schema, [variableDefinition], RawVariables.of([variable: inputValue])) + def resolvedValues = ValuesResolver.coerceVariableValues(schema, [variableDefinition], RawVariables.of([variable: inputValue]), graphQLContext, locale) then: resolvedValues.get('variable') == outputValue @@ -51,8 +56,8 @@ class ValuesResolverTest extends Specification { inputType | variableType | inputValue || outputValue GraphQLInt | new TypeName("Int") | 100 || 100 GraphQLString | new TypeName("String") | 'someString' || 'someString' - GraphQLBoolean | new TypeName("Boolean") | 'true' || true - GraphQLFloat | new TypeName("Float") | '42.43' || 42.43d + GraphQLBoolean | new TypeName("Boolean") | true || true + GraphQLFloat | new TypeName("Float") | 42.43d || 42.43d } def "getVariableValues: map object as variable input"() { @@ -72,7 +77,7 @@ class ValuesResolverTest extends Specification { VariableDefinition variableDefinition = new VariableDefinition("variable", new TypeName("Person")) when: - def resolvedValues = ValuesResolver.coerceVariableValues(schema, [variableDefinition], RawVariables.of([variable: inputValue])) + def resolvedValues = ValuesResolver.coerceVariableValues(schema, [variableDefinition], RawVariables.of([variable: inputValue]), graphQLContext, locale) then: resolvedValues.get('variable') == outputValue where: @@ -112,7 +117,7 @@ class ValuesResolverTest extends Specification { when: def obj = new Person('a', 123) - ValuesResolver.coerceVariableValues(schema, [variableDefinition], RawVariables.of([variable: obj])) + ValuesResolver.coerceVariableValues(schema, [variableDefinition], RawVariables.of([variable: obj]), graphQLContext, locale) then: thrown(CoercingParseValueException) } @@ -123,7 +128,7 @@ class ValuesResolverTest extends Specification { VariableDefinition variableDefinition = new VariableDefinition("variable", new ListType(new TypeName("String"))) String value = "world" when: - def resolvedValues = ValuesResolver.coerceVariableValues(schema, [variableDefinition], RawVariables.of([variable: value])) + def resolvedValues = ValuesResolver.coerceVariableValues(schema, [variableDefinition], RawVariables.of([variable: value]), graphQLContext, locale) then: resolvedValues.get('variable') == ['world'] } @@ -134,7 +139,7 @@ class ValuesResolverTest extends Specification { VariableDefinition variableDefinition = new VariableDefinition("variable", new ListType(new TypeName("String"))) List value = ["hello","world"] when: - def resolvedValues = ValuesResolver.coerceVariableValues(schema, [variableDefinition], RawVariables.of([variable: value])) + def resolvedValues = ValuesResolver.coerceVariableValues(schema, [variableDefinition], RawVariables.of([variable: value]), graphQLContext, locale) then: resolvedValues.get('variable') == ['hello','world'] } @@ -145,7 +150,7 @@ class ValuesResolverTest extends Specification { VariableDefinition variableDefinition = new VariableDefinition("variable", new ListType(new TypeName("String"))) String[] value = ["hello","world"] as String[] when: - def resolvedValues = ValuesResolver.coerceVariableValues(schema, [variableDefinition], RawVariables.of([variable: value])) + def resolvedValues = ValuesResolver.coerceVariableValues(schema, [variableDefinition], RawVariables.of([variable: value]), graphQLContext, locale) then: resolvedValues.get('variable') == ['hello','world'] } @@ -157,7 +162,7 @@ class ValuesResolverTest extends Specification { def argument = new Argument("arg", new VariableReference("var")) when: - def values = ValuesResolver.getArgumentValues([fieldArgument], [argument], variables) + def values = ValuesResolver.getArgumentValues([fieldArgument], [argument], variables, graphQLContext, locale) then: values['arg'] == 'hello' @@ -167,17 +172,20 @@ class ValuesResolverTest extends Specification { given: "schema defining input object" def inputObjectType = newInputObject() .name("inputObject") + .field(newInputObjectField() + .name("inputField") + .type(GraphQLString)) .build() - def fieldArgument = newArgument().name("arg").type(inputObjectType).defaultValue("hello").build() + def fieldArgument = newArgument().name("arg").type(inputObjectType).defaultValueProgrammatic([inputField: "hello"]).build() def argument = new Argument("arg", new VariableReference("var")) when: def variables = CoercedVariables.emptyVariables() - def values = ValuesResolver.getArgumentValues([fieldArgument], [argument], variables) + def values = ValuesResolver.getArgumentValues([fieldArgument], [argument], variables, graphQLContext, locale) then: - values['arg'] == 'hello' + values['arg'] == [inputField: 'hello'] } def "getArgumentValues: resolves object literal"() { @@ -204,7 +212,7 @@ class ValuesResolverTest extends Specification { when: def argument = new Argument("arg", inputValue) - def values = ValuesResolver.getArgumentValues([fieldArgument], [argument], CoercedVariables.emptyVariables()) + def values = ValuesResolver.getArgumentValues([fieldArgument], [argument], CoercedVariables.emptyVariables(), graphQLContext, locale) then: values['arg'] == outputValue @@ -240,19 +248,19 @@ class ValuesResolverTest extends Specification { .field(newInputObjectField() .name("intKey") .type(nonNull(GraphQLInt)) - .defaultValue(3) + .defaultValueProgrammatic(3) .build()) .field(newInputObjectField() .name("stringKey") .type(GraphQLString) - .defaultValue("defaultString") + .defaultValueProgrammatic("defaultString") .build()) .build() def fieldArgument = newArgument().name("arg").type(inputObjectType).build() when: def argument = new Argument("arg", inputValue) - def values = ValuesResolver.getArgumentValues([fieldArgument], [argument], CoercedVariables.emptyVariables()) + def values = ValuesResolver.getArgumentValues([fieldArgument], [argument], CoercedVariables.emptyVariables(), graphQLContext, locale) then: values['arg'] == outputValue @@ -300,7 +308,7 @@ class ValuesResolverTest extends Specification { def fieldArgument1 = newArgument().name("arg1").type(enumType).build() def fieldArgument2 = newArgument().name("arg2").type(enumType).build() when: - def values = ValuesResolver.getArgumentValues([fieldArgument1, fieldArgument2], [argument1, argument2], CoercedVariables.emptyVariables()) + def values = ValuesResolver.getArgumentValues([fieldArgument1, fieldArgument2], [argument1, argument2], CoercedVariables.emptyVariables(), graphQLContext, locale) then: values['arg1'] == 'PLUTO' @@ -317,7 +325,7 @@ class ValuesResolverTest extends Specification { def fieldArgument = newArgument().name("arg").type(list(GraphQLBoolean)).build() when: - def values = ValuesResolver.getArgumentValues([fieldArgument], [argument], CoercedVariables.emptyVariables()) + def values = ValuesResolver.getArgumentValues([fieldArgument], [argument], CoercedVariables.emptyVariables(), graphQLContext, locale) then: values['arg'] == [true, false] @@ -331,7 +339,7 @@ class ValuesResolverTest extends Specification { def fieldArgument = newArgument().name("arg").type(list(GraphQLString)).build() when: - def values = ValuesResolver.getArgumentValues([fieldArgument], [argument], CoercedVariables.emptyVariables()) + def values = ValuesResolver.getArgumentValues([fieldArgument], [argument], CoercedVariables.emptyVariables(), graphQLContext, locale) then: values['arg'] == ['world'] @@ -349,7 +357,7 @@ class ValuesResolverTest extends Specification { VariableDefinition variableDefinition = new VariableDefinition("variable", new TypeName("Test")) when: - def resolvedValues = ValuesResolver.coerceVariableValues(schema, [variableDefinition], RawVariables.of([variable: inputValue])) + def resolvedValues = ValuesResolver.coerceVariableValues(schema, [variableDefinition], RawVariables.of([variable: inputValue]), graphQLContext, locale) then: resolvedValues.get('variable') == outputValue where: @@ -370,14 +378,14 @@ class ValuesResolverTest extends Specification { .field(newInputObjectField() .name("stringKey") .type(GraphQLString) - .defaultValue("defaultString")) + .defaultValueProgrammatic("defaultString")) .build() def schema = TestUtil.schemaWithInputType(inputObjectType) VariableDefinition variableDefinition = new VariableDefinition("variable", new TypeName("InputObject")) when: - def resolvedValues = ValuesResolver.coerceVariableValues(schema, [variableDefinition], RawVariables.of([variable: inputValue])) + def resolvedValues = ValuesResolver.coerceVariableValues(schema, [variableDefinition], RawVariables.of([variable: inputValue]), graphQLContext, locale) then: resolvedValues.get('variable') == outputValue @@ -404,7 +412,7 @@ class ValuesResolverTest extends Specification { VariableDefinition variableDefinition = new VariableDefinition("variable", new TypeName("InputObject")) when: - ValuesResolver.coerceVariableValues(schema, [variableDefinition], RawVariables.of([variable: inputValue])) + ValuesResolver.coerceVariableValues(schema, [variableDefinition], RawVariables.of([variable: inputValue]), graphQLContext, locale) then: thrown(GraphQLException) @@ -423,7 +431,7 @@ class ValuesResolverTest extends Specification { VariableDefinition barVarDef = new VariableDefinition("bar", new TypeName("String")) when: - def resolvedValues = ValuesResolver.coerceVariableValues(schema, [fooVarDef, barVarDef], RawVariables.of(InputValue)) + def resolvedValues = ValuesResolver.coerceVariableValues(schema, [fooVarDef, barVarDef], RawVariables.of(InputValue), graphQLContext, locale) then: resolvedValues.toMap() == outputValue @@ -441,7 +449,7 @@ class ValuesResolverTest extends Specification { VariableDefinition fooVarDef = new VariableDefinition("foo", new NonNullType(new TypeName("String"))) when: - ValuesResolver.coerceVariableValues(schema, [fooVarDef], RawVariables.emptyVariables()) + ValuesResolver.coerceVariableValues(schema, [fooVarDef], RawVariables.emptyVariables(), graphQLContext, locale) then: thrown(GraphQLException) @@ -460,7 +468,7 @@ class ValuesResolverTest extends Specification { def variableValuesMap = RawVariables.of(["foo": null, "bar": "barValue"]) when: - def resolvedVars = ValuesResolver.coerceVariableValues(schema, [fooVarDef, barVarDef], variableValuesMap) + def resolvedVars = ValuesResolver.coerceVariableValues(schema, [fooVarDef, barVarDef], variableValuesMap, graphQLContext, locale) then: resolvedVars.get('foo') == null @@ -477,7 +485,7 @@ class ValuesResolverTest extends Specification { def variableValuesMap = RawVariables.of(["foo": null]) when: - ValuesResolver.coerceVariableValues(schema, [fooVarDef], variableValuesMap) + ValuesResolver.coerceVariableValues(schema, [fooVarDef], variableValuesMap, graphQLContext, locale) then: def error = thrown(NonNullableValueCoercedAsNullException) @@ -495,7 +503,7 @@ class ValuesResolverTest extends Specification { def variableValuesMap = RawVariables.of(["foo": [null]]) when: - ValuesResolver.coerceVariableValues(schema, [fooVarDef], variableValuesMap) + ValuesResolver.coerceVariableValues(schema, [fooVarDef], variableValuesMap, graphQLContext, locale) then: def error = thrown(NonNullableValueCoercedAsNullException) @@ -510,12 +518,12 @@ class ValuesResolverTest extends Specification { .name("inputObject") .build() - def fieldArgument = newArgument().name("arg").type(inputObjectType).defaultValue("hello").build() + def fieldArgument = newArgument().name("arg").type(inputObjectType).defaultValueProgrammatic("hello").build() def argument = new Argument("arg", NullValue.newNullValue().build()) when: def variables = CoercedVariables.emptyVariables() - def values = ValuesResolver.getArgumentValues([fieldArgument], [argument], variables) + def values = ValuesResolver.getArgumentValues([fieldArgument], [argument], variables, graphQLContext, locale) then: values['arg'] == null @@ -527,12 +535,12 @@ class ValuesResolverTest extends Specification { .name("inputObject") .build() - def fieldArgument = newArgument().name("arg").type(inputObjectType).defaultValue("hello").build() + def fieldArgument = newArgument().name("arg").type(inputObjectType).defaultValueProgrammatic("hello").build() def argument = new Argument("arg", new VariableReference("var")) when: def variables = CoercedVariables.of(["var": null]) - def values = ValuesResolver.getArgumentValues([fieldArgument], [argument], variables) + def values = ValuesResolver.getArgumentValues([fieldArgument], [argument], variables, graphQLContext, locale) then: values['arg'] == null @@ -549,7 +557,7 @@ class ValuesResolverTest extends Specification { when: def variables = CoercedVariables.of(["var": null]) - ValuesResolver.getArgumentValues([fieldArgument], [argument], variables) + ValuesResolver.getArgumentValues([fieldArgument], [argument], variables, graphQLContext, locale) then: def error = thrown(NonNullableValueCoercedAsNullException) @@ -595,7 +603,7 @@ class ValuesResolverTest extends Specification { executionResult.data == null executionResult.errors.size() == 1 executionResult.errors[0].errorType == ErrorType.ValidationError - executionResult.errors[0].message == 'Variable \'input\' has an invalid value: Invalid input for Enum \'PositionType\'. No value found for name \'UNKNOWN_POSITION\'' + executionResult.errors[0].message == "Variable 'input' has an invalid value: Invalid input for enum 'PositionType'. No value found for name 'UNKNOWN_POSITION'" executionResult.errors[0].locations == [new SourceLocation(2, 35)] } @@ -633,7 +641,7 @@ class ValuesResolverTest extends Specification { executionResult.data == null executionResult.errors.size() == 1 executionResult.errors[0].errorType == ErrorType.ValidationError - executionResult.errors[0].message == 'Variable \'input\' has an invalid value: Expected type \'Boolean\' but was \'String\'.' + executionResult.errors[0].message == "Variable 'input' has an invalid value: Expected a Boolean input, but it was a 'String'" executionResult.errors[0].locations == [new SourceLocation(2, 35)] } @@ -671,7 +679,7 @@ class ValuesResolverTest extends Specification { executionResult.data == null executionResult.errors.size() == 1 executionResult.errors[0].errorType == ErrorType.ValidationError - executionResult.errors[0].message == 'Variable \'input\' has an invalid value: Expected type \'Float\' but was \'String\'.' + executionResult.errors[0].message == "Variable 'input' has an invalid value: Expected a Number input, but it was a 'String'" executionResult.errors[0].locations == [new SourceLocation(2, 35)] } } \ No newline at end of file diff --git a/src/test/groovy/graphql/execution/ValuesResolverTestLegacy.groovy b/src/test/groovy/graphql/execution/ValuesResolverTestLegacy.groovy index e52d4b216c..0c944b7bb1 100644 --- a/src/test/groovy/graphql/execution/ValuesResolverTestLegacy.groovy +++ b/src/test/groovy/graphql/execution/ValuesResolverTestLegacy.groovy @@ -1,6 +1,13 @@ -package graphql.language - - +package graphql.execution + +import graphql.GraphQLContext +import graphql.language.ArrayValue +import graphql.language.EnumValue +import graphql.language.FloatValue +import graphql.language.IntValue +import graphql.language.ObjectField +import graphql.language.ObjectValue +import graphql.language.StringValue import graphql.schema.GraphQLEnumType import graphql.schema.GraphQLInputObjectType import spock.lang.Ignore @@ -11,27 +18,30 @@ import static graphql.Scalars.GraphQLFloat import static graphql.Scalars.GraphQLID import static graphql.Scalars.GraphQLInt import static graphql.Scalars.GraphQLString -import static graphql.execution.ValuesResolver.valueToLiteralLegacy +import static graphql.execution.ValuesResolverLegacy.valueToLiteralLegacy import static graphql.language.BooleanValue.newBooleanValue import static graphql.schema.GraphQLList.list import static graphql.schema.GraphQLNonNull.nonNull class ValuesResolverTestLegacy extends Specification { + def graphQLContext = GraphQLContext.getDefault() + def locale = Locale.getDefault() + def 'converts boolean values to ASTs'() { expect: - valueToLiteralLegacy(true, GraphQLBoolean).isEqualTo(newBooleanValue(true).build()) + valueToLiteralLegacy(true, GraphQLBoolean, graphQLContext, locale).isEqualTo(newBooleanValue(true).build()) - valueToLiteralLegacy(false, GraphQLBoolean).isEqualTo(newBooleanValue(false).build()) + valueToLiteralLegacy(false, GraphQLBoolean, graphQLContext, locale).isEqualTo(newBooleanValue(false).build()) - valueToLiteralLegacy(null, GraphQLBoolean) == null + valueToLiteralLegacy(null, GraphQLBoolean, graphQLContext, locale) == null - valueToLiteralLegacy(0, GraphQLBoolean).isEqualTo(newBooleanValue(false).build()) + valueToLiteralLegacy(0, GraphQLBoolean, graphQLContext, locale).isEqualTo(newBooleanValue(false).build()) - valueToLiteralLegacy(1, GraphQLBoolean).isEqualTo(newBooleanValue(true).build()) + valueToLiteralLegacy(1, GraphQLBoolean, graphQLContext, locale).isEqualTo(newBooleanValue(true).build()) def NonNullBoolean = nonNull(GraphQLBoolean) - valueToLiteralLegacy(0, NonNullBoolean).isEqualTo(newBooleanValue(false).build()) + valueToLiteralLegacy(0, NonNullBoolean, graphQLContext, locale).isEqualTo(newBooleanValue(false).build()) } BigInteger bigInt(int i) { @@ -40,60 +50,60 @@ class ValuesResolverTestLegacy extends Specification { def 'converts Int values to Int ASTs'() { expect: - valueToLiteralLegacy(123.0, GraphQLInt).isEqualTo(IntValue.newIntValue(bigInt(123)).build()) + valueToLiteralLegacy(123.0, GraphQLInt, graphQLContext, locale).isEqualTo(IntValue.newIntValue(bigInt(123)).build()) - valueToLiteralLegacy(1e4, GraphQLInt).isEqualTo(IntValue.newIntValue(bigInt(10000)).build()) + valueToLiteralLegacy(1e4, GraphQLInt, graphQLContext, locale).isEqualTo(IntValue.newIntValue(bigInt(10000)).build()) } def 'converts Float values to Int/Float ASTs'() { expect: - valueToLiteralLegacy(123.0, GraphQLFloat).isEqualTo(FloatValue.newFloatValue(123.0).build()) + valueToLiteralLegacy(123.0, GraphQLFloat, graphQLContext, locale).isEqualTo(FloatValue.newFloatValue(123.0).build()) - valueToLiteralLegacy(123.5, GraphQLFloat).isEqualTo(FloatValue.newFloatValue(123.5).build()) + valueToLiteralLegacy(123.5, GraphQLFloat, graphQLContext, locale).isEqualTo(FloatValue.newFloatValue(123.5).build()) - valueToLiteralLegacy(1e4, GraphQLFloat).isEqualTo(FloatValue.newFloatValue(10000.0).build()) + valueToLiteralLegacy(1e4, GraphQLFloat, graphQLContext, locale).isEqualTo(FloatValue.newFloatValue(10000.0).build()) - valueToLiteralLegacy(1e40, GraphQLFloat).isEqualTo(FloatValue.newFloatValue(1.0e40).build()) + valueToLiteralLegacy(1e40, GraphQLFloat, graphQLContext, locale).isEqualTo(FloatValue.newFloatValue(1.0e40).build()) } def 'converts String values to String ASTs'() { expect: - valueToLiteralLegacy('hello', GraphQLString).isEqualTo(new StringValue('hello')) + valueToLiteralLegacy('hello', GraphQLString, graphQLContext, locale).isEqualTo(new StringValue('hello')) - valueToLiteralLegacy('VALUE', GraphQLString).isEqualTo(new StringValue('VALUE')) + valueToLiteralLegacy('VALUE', GraphQLString, graphQLContext, locale).isEqualTo(new StringValue('VALUE')) - valueToLiteralLegacy('VA\n\t\f\r\b\\LUE', GraphQLString).isEqualTo(new StringValue('VA\n\t\f\r\b\\LUE')) + valueToLiteralLegacy('VA\n\t\f\r\b\\LUE', GraphQLString, graphQLContext, locale).isEqualTo(new StringValue('VA\n\t\f\r\b\\LUE')) - valueToLiteralLegacy('VA\\L\"UE', GraphQLString).isEqualTo(new StringValue('VA\\L\"UE')) + valueToLiteralLegacy('VA\\L\"UE', GraphQLString, graphQLContext, locale).isEqualTo(new StringValue('VA\\L\"UE')) - valueToLiteralLegacy(123, GraphQLString).isEqualTo(new StringValue('123')) + valueToLiteralLegacy(123, GraphQLString, graphQLContext, locale).isEqualTo(new StringValue('123')) - valueToLiteralLegacy(false, GraphQLString).isEqualTo(new StringValue('false')) + valueToLiteralLegacy(false, GraphQLString, graphQLContext, locale).isEqualTo(new StringValue('false')) - valueToLiteralLegacy(null, GraphQLString) == null + valueToLiteralLegacy(null, GraphQLString, graphQLContext, locale) == null } def 'converts ID values to Int/String ASTs'() { expect: - valueToLiteralLegacy('hello', GraphQLID).isEqualTo(new StringValue('hello')) + valueToLiteralLegacy('hello', GraphQLID, graphQLContext, locale).isEqualTo(new StringValue('hello')) - valueToLiteralLegacy('VALUE', GraphQLID).isEqualTo(new StringValue('VALUE')) + valueToLiteralLegacy('VALUE', GraphQLID, graphQLContext, locale).isEqualTo(new StringValue('VALUE')) // Note: EnumValues cannot contain non-identifier characters - valueToLiteralLegacy('VA\nLUE', GraphQLID).isEqualTo(new StringValue('VA\nLUE')) + valueToLiteralLegacy('VA\nLUE', GraphQLID, graphQLContext, locale).isEqualTo(new StringValue('VA\nLUE')) // Note: IntValues are used when possible. - valueToLiteralLegacy(123, GraphQLID).isEqualTo(new IntValue(bigInt(123))) + valueToLiteralLegacy(123, GraphQLID, graphQLContext, locale).isEqualTo(new IntValue(bigInt(123))) - valueToLiteralLegacy(null, GraphQLID) == null + valueToLiteralLegacy(null, GraphQLID, graphQLContext, locale) == null } def 'does not converts NonNull values to NullValue'() { expect: def NonNullBoolean = nonNull(GraphQLBoolean) - valueToLiteralLegacy(null, NonNullBoolean) == null + valueToLiteralLegacy(null, NonNullBoolean, graphQLContext, locale) == null } def complexValue = { someArbitrary: 'complexValue' } @@ -107,42 +117,42 @@ class ValuesResolverTestLegacy extends Specification { def 'converts string values to Enum ASTs if possible'() { expect: - valueToLiteralLegacy('HELLO', myEnum).isEqualTo(new EnumValue('HELLO')) + valueToLiteralLegacy('HELLO', myEnum, graphQLContext, locale).isEqualTo(new EnumValue('HELLO')) - valueToLiteralLegacy(complexValue, myEnum).isEqualTo(new EnumValue('COMPLEX')) + valueToLiteralLegacy(complexValue, myEnum, graphQLContext, locale).isEqualTo(new EnumValue('COMPLEX')) } def 'converts array values to List ASTs'() { expect: - valueToLiteralLegacy(['FOO', 'BAR'], list(GraphQLString)).isEqualTo( + valueToLiteralLegacy(['FOO', 'BAR'], list(GraphQLString), graphQLContext, locale).isEqualTo( new ArrayValue([new StringValue('FOO'), new StringValue('BAR')]) ) - valueToLiteralLegacy(['HELLO', 'GOODBYE'], list(myEnum)).isEqualTo( + valueToLiteralLegacy(['HELLO', 'GOODBYE'], list(myEnum), graphQLContext, locale).isEqualTo( new ArrayValue([new EnumValue('HELLO'), new EnumValue('GOODBYE')]) ) } def 'converts list singletons'() { expect: - valueToLiteralLegacy('FOO', list(GraphQLString)).isEqualTo( + valueToLiteralLegacy('FOO', list(GraphQLString), graphQLContext, locale).isEqualTo( new StringValue('FOO') ) } def 'converts list to lists'() { expect: - valueToLiteralLegacy(['hello', 'world'], list(GraphQLString)).isEqualTo( - new ArrayValue(['hello', 'world']) + valueToLiteralLegacy(['hello', 'world'], list(GraphQLString), graphQLContext, locale).isEqualTo( + new ArrayValue([new StringValue('hello'), new StringValue('world')]) ) } def 'converts arrays to lists'() { String[] sArr = ['hello', 'world'] as String[] expect: - valueToLiteralLegacy(sArr, list(GraphQLString)).isEqualTo( - new ArrayValue(['hello', 'world']) + valueToLiteralLegacy(sArr, list(GraphQLString), graphQLContext, locale).isEqualTo( + new ArrayValue([new StringValue('hello'), new StringValue('world')]) ) } @@ -165,19 +175,19 @@ class ValuesResolverTestLegacy extends Specification { .build() expect: - valueToLiteralLegacy([foo: 3, bar: 'HELLO'], inputObj).isEqualTo( + valueToLiteralLegacy([foo: 3, bar: 'HELLO'], inputObj, graphQLContext, locale).isEqualTo( new ObjectValue([new ObjectField("foo", new IntValue(bigInt(3))), new ObjectField("bar", new EnumValue('HELLO')), ]) ) - valueToLiteralLegacy(new SomePojo(), inputObj).isEqualTo( + valueToLiteralLegacy(new SomePojo(), inputObj, graphQLContext, locale).isEqualTo( new ObjectValue([new ObjectField("foo", new IntValue(bigInt(3))), new ObjectField("bar", new EnumValue('HELLO')), ]) ) - valueToLiteralLegacy(new SomePojoWithFields(), inputObj).isEqualTo( + valueToLiteralLegacy(new SomePojoWithFields(), inputObj, graphQLContext, locale).isEqualTo( new ObjectValue([new ObjectField("foo", new IntValue(bigInt(3))), new ObjectField("bar", new EnumValue('HELLO')), ]) @@ -195,7 +205,7 @@ class ValuesResolverTestLegacy extends Specification { .field({ f -> f.name("bar").type(myEnum) }) .build() - valueToLiteralLegacy([foo: null], inputObj).isEqualTo( + valueToLiteralLegacy([foo: null], inputObj, graphQLContext, locale).isEqualTo( new ObjectValue([new ObjectField("foo", null)]) ) } diff --git a/src/test/groovy/graphql/execution/directives/QueryDirectivesImplTest.groovy b/src/test/groovy/graphql/execution/directives/QueryDirectivesImplTest.groovy index a728a99d6a..1c25262b48 100644 --- a/src/test/groovy/graphql/execution/directives/QueryDirectivesImplTest.groovy +++ b/src/test/groovy/graphql/execution/directives/QueryDirectivesImplTest.groovy @@ -1,5 +1,6 @@ package graphql.execution.directives +import graphql.GraphQLContext import graphql.TestUtil import graphql.execution.MergedField import spock.lang.Specification @@ -30,7 +31,7 @@ class QueryDirectivesImplTest extends Specification { def mergedField = MergedField.newMergedField([f1, f2]).build() - def impl = new QueryDirectivesImpl(mergedField, schema, [var: 10]) + def impl = new QueryDirectivesImpl(mergedField, schema, [var: 10], GraphQLContext.getDefault(), Locale.getDefault()) when: def directives = impl.getImmediateDirectivesByName() @@ -47,10 +48,10 @@ class QueryDirectivesImplTest extends Specification { result[0].getName() == "cached" result[1].getName() == "cached" - result[0].getArgument("forMillis").getArgumentValue().value == 99 // defaults + result[0].getArgument("forMillis").getArgumentValue().value == 99 // defaults. Retain deprecated method to test getImmediateDirective printAst(result[0].getArgument("forMillis").getArgumentDefaultValue().getValue()) == "99" - result[1].getArgument("forMillis").getArgumentValue().value == 10 + result[1].getArgument("forMillis").getArgumentValue().value == 10 // Retain deprecated method to test getImmediateDirective printAst(result[1].getArgument("forMillis").getArgumentDefaultValue().getValue()) == "99" // the prototypical other properties are copied ok diff --git a/src/test/groovy/graphql/execution/directives/QueryDirectivesIntegrationTest.groovy b/src/test/groovy/graphql/execution/directives/QueryDirectivesIntegrationTest.groovy index c1e09c52eb..89a5d85a07 100644 --- a/src/test/groovy/graphql/execution/directives/QueryDirectivesIntegrationTest.groovy +++ b/src/test/groovy/graphql/execution/directives/QueryDirectivesIntegrationTest.groovy @@ -3,7 +3,6 @@ package graphql.execution.directives import graphql.GraphQL import graphql.TestUtil import graphql.schema.DataFetcher -import graphql.schema.GraphQLDirective import spock.lang.Specification /** @@ -74,7 +73,7 @@ class QueryDirectivesIntegrationTest extends Specification { graphql.execute({ input -> input.query(query).root(root) }) } - def joinArgs(List timeoutDirectives) { + static def joinArgs(List timeoutDirectives) { timeoutDirectives.collect({ def s = it.getName() + "(" it.arguments.forEach({ @@ -95,7 +94,7 @@ class QueryDirectivesIntegrationTest extends Specification { then: er.errors.isEmpty() - Map> immediateMap = capturedDirectives["review"].getImmediateDirectivesByName() + def immediateMap = capturedDirectives["review"].getImmediateAppliedDirectivesByName() def entries = immediateMap.entrySet().collectEntries({ [(it.getKey()): joinArgs(it.getValue())] }) @@ -103,7 +102,7 @@ class QueryDirectivesIntegrationTest extends Specification { timeout: "timeout(afterMillis:5),timeout(afterMillis:28),timeout(afterMillis:10)" ] - def immediate = capturedDirectives["review"].getImmediateDirective("cached") + def immediate = capturedDirectives["review"].getImmediateAppliedDirective("cached") joinArgs(immediate) == "cached(forMillis:5),cached(forMillis:10)" } @@ -123,7 +122,7 @@ class QueryDirectivesIntegrationTest extends Specification { then: er.errors.isEmpty() - Map> immediateMap = capturedDirectives["title"].getImmediateDirectivesByName() + def immediateMap = capturedDirectives["title"].getImmediateAppliedDirectivesByName() def entries = immediateMap.entrySet().collectEntries({ [(it.getKey()): joinArgs(it.getValue())] }) @@ -131,7 +130,7 @@ class QueryDirectivesIntegrationTest extends Specification { timeout: "timeout(afterMillis:99)" ] - def immediate = capturedDirectives["review"].getImmediateDirective("cached") + def immediate = capturedDirectives["review"].getImmediateAppliedDirective("cached") joinArgs(immediate) == "cached(forMillis:10)" } diff --git a/src/test/groovy/graphql/execution/instrumentation/ChainedInstrumentationStateTest.groovy b/src/test/groovy/graphql/execution/instrumentation/ChainedInstrumentationStateTest.groovy index 90c858e9c4..c2c1ffb8ca 100644 --- a/src/test/groovy/graphql/execution/instrumentation/ChainedInstrumentationStateTest.groovy +++ b/src/test/groovy/graphql/execution/instrumentation/ChainedInstrumentationStateTest.groovy @@ -252,6 +252,33 @@ class ChainedInstrumentationStateTest extends Specification { } + def "single chain"() { + def a = new NamedInstrumentation("A") + def chainedInstrumentation = new ChainedInstrumentation([a]) + + def query = """ + query HeroNameAndFriendsQuery { + hero { + id + } + } + """ + + when: + def graphQL = GraphQL + .newGraphQL(StarWarsSchema.starWarsSchema) + .instrumentation(chainedInstrumentation) + .build() + + graphQL.execute(query) + + then: + noExceptionThrown() + + assertCalls(a) + + } + private void assertCalls(NamedInstrumentation instrumentation) { assert instrumentation.dfInvocations[0].getFieldDefinition().name == 'hero' assert instrumentation.dfInvocations[0].getExecutionStepInfo().getPath().toList() == ['hero'] diff --git a/src/test/groovy/graphql/execution/instrumentation/InstrumentationTest.groovy b/src/test/groovy/graphql/execution/instrumentation/InstrumentationTest.groovy index f5e74eb510..694e100c0d 100644 --- a/src/test/groovy/graphql/execution/instrumentation/InstrumentationTest.groovy +++ b/src/test/groovy/graphql/execution/instrumentation/InstrumentationTest.groovy @@ -15,6 +15,7 @@ import graphql.schema.DataFetchingEnvironment import graphql.schema.PropertyDataFetcher import graphql.schema.StaticDataFetcher import org.awaitility.Awaitility +import org.jetbrains.annotations.NotNull import spock.lang.Specification import java.util.concurrent.CompletableFuture @@ -158,7 +159,7 @@ class InstrumentationTest extends Specification { * java-dataloader works. That is calls inside DataFetchers are "batched" * until a "dispatch" signal is made. */ - class WaitingInstrumentation extends SimpleInstrumentation { + class WaitingInstrumentation extends SimplePerformantInstrumentation { final AtomicBoolean goSignal = new AtomicBoolean() @@ -180,6 +181,7 @@ class InstrumentationTest extends Specification { } } + @NotNull @Override DataFetcher instrumentDataFetcher(DataFetcher dataFetcher, InstrumentationFieldFetchParameters parameters, InstrumentationState state) { System.out.println(String.format("t%s instrument DF for %s", Thread.currentThread().getId(), parameters.environment.getExecutionStepInfo().getPath())) diff --git a/src/test/groovy/graphql/execution/instrumentation/LegacyTestingInstrumentation.groovy b/src/test/groovy/graphql/execution/instrumentation/LegacyTestingInstrumentation.groovy index 5e24cbf09f..e8a9478cb6 100644 --- a/src/test/groovy/graphql/execution/instrumentation/LegacyTestingInstrumentation.groovy +++ b/src/test/groovy/graphql/execution/instrumentation/LegacyTestingInstrumentation.groovy @@ -39,85 +39,85 @@ class LegacyTestingInstrumentation implements Instrumentation { @Override InstrumentationContext beginExecution(InstrumentationExecutionParameters parameters) { - assert parameters.getInstrumentationState() == instrumentationState + assert parameters.getInstrumentationState() == instrumentationState // Retain for test coverage new TestingInstrumentContext("execution", executionList, throwableList, useOnDispatch) } @Override InstrumentationContext beginParse(InstrumentationExecutionParameters parameters) { - assert parameters.getInstrumentationState() == instrumentationState + assert parameters.getInstrumentationState() == instrumentationState // Retain for test coverage return new TestingInstrumentContext("parse", executionList, throwableList, useOnDispatch) } @Override InstrumentationContext> beginValidation(InstrumentationValidationParameters parameters) { - assert parameters.getInstrumentationState() == instrumentationState + assert parameters.getInstrumentationState() == instrumentationState // Retain for test coverage return new TestingInstrumentContext("validation", executionList, throwableList, useOnDispatch) } @Override ExecutionStrategyInstrumentationContext beginExecutionStrategy(InstrumentationExecutionStrategyParameters parameters) { - assert parameters.getInstrumentationState() == instrumentationState + assert parameters.getInstrumentationState() == instrumentationState // Retain for test coverage return new TestingExecutionStrategyInstrumentationContext("execution-strategy", executionList, throwableList, useOnDispatch) } @Override InstrumentationContext beginExecuteOperation(InstrumentationExecuteOperationParameters parameters) { - assert parameters.getInstrumentationState() == instrumentationState + assert parameters.getInstrumentationState() == instrumentationState // Retain for test coverage return new TestingInstrumentContext("execute-operation", executionList, throwableList, useOnDispatch) } @Override InstrumentationContext beginSubscribedFieldEvent(InstrumentationFieldParameters parameters) { - assert parameters.getInstrumentationState() == instrumentationState + assert parameters.getInstrumentationState() == instrumentationState // Retain for test coverage return new TestingInstrumentContext("subscribed-field-event-$parameters.field.name", executionList, throwableList, useOnDispatch) } @Override InstrumentationContext beginField(InstrumentationFieldParameters parameters) { - assert parameters.getInstrumentationState() == instrumentationState + assert parameters.getInstrumentationState() == instrumentationState // Retain for test coverage return new TestingInstrumentContext("field-$parameters.field.name", executionList, throwableList, useOnDispatch) } @Override InstrumentationContext beginFieldFetch(InstrumentationFieldFetchParameters parameters) { - assert parameters.getInstrumentationState() == instrumentationState + assert parameters.getInstrumentationState() == instrumentationState // Retain for test coverage return new TestingInstrumentContext("fetch-$parameters.field.name", executionList, throwableList, useOnDispatch) } @Override InstrumentationContext> beginFieldComplete(InstrumentationFieldCompleteParameters parameters) { - assert parameters.getInstrumentationState() == instrumentationState + assert parameters.getInstrumentationState() == instrumentationState // Retain for test coverage return new TestingInstrumentContext("complete-$parameters.field.name", executionList, throwableList, useOnDispatch) } @Override InstrumentationContext> beginFieldListComplete(InstrumentationFieldCompleteParameters parameters) { - assert parameters.getInstrumentationState() == instrumentationState + assert parameters.getInstrumentationState() == instrumentationState // Retain for test coverage return new TestingInstrumentContext("complete-list-$parameters.field.name", executionList, throwableList, useOnDispatch) } @Override GraphQLSchema instrumentSchema(GraphQLSchema schema, InstrumentationExecutionParameters parameters) { - assert parameters.getInstrumentationState() == instrumentationState + assert parameters.getInstrumentationState() == instrumentationState // Retain for test coverage return schema } @Override ExecutionInput instrumentExecutionInput(ExecutionInput executionInput, InstrumentationExecutionParameters parameters) { - assert parameters.getInstrumentationState() == instrumentationState + assert parameters.getInstrumentationState() == instrumentationState // Retain for test coverage return executionInput } @Override ExecutionContext instrumentExecutionContext(ExecutionContext executionContext, InstrumentationExecutionParameters parameters) { - assert parameters.getInstrumentationState() == instrumentationState + assert parameters.getInstrumentationState() == instrumentationState // Retain for test coverage return executionContext } @Override DataFetcher instrumentDataFetcher(DataFetcher dataFetcher, InstrumentationFieldFetchParameters parameters) { - assert parameters.getInstrumentationState() == instrumentationState + assert parameters.getInstrumentationState() == instrumentationState // Retain for test coverage dfClasses.add(dataFetcher.getClass()) return new DataFetcher() { @Override @@ -130,7 +130,7 @@ class LegacyTestingInstrumentation implements Instrumentation { @Override CompletableFuture instrumentExecutionResult(ExecutionResult executionResult, InstrumentationExecutionParameters parameters) { - assert parameters.getInstrumentationState() == instrumentationState + assert parameters.getInstrumentationState() == instrumentationState // Retain for test coverage return CompletableFuture.completedFuture(executionResult) } } diff --git a/src/test/groovy/graphql/execution/instrumentation/dataloader/BatchCompareDataFetchers.java b/src/test/groovy/graphql/execution/instrumentation/dataloader/BatchCompareDataFetchers.java index 0569e667ee..430f79a74c 100644 --- a/src/test/groovy/graphql/execution/instrumentation/dataloader/BatchCompareDataFetchers.java +++ b/src/test/groovy/graphql/execution/instrumentation/dataloader/BatchCompareDataFetchers.java @@ -6,6 +6,7 @@ import graphql.schema.DataFetcher; import org.dataloader.BatchLoader; import org.dataloader.DataLoader; +import org.dataloader.DataLoaderFactory; import java.util.ArrayList; import java.util.Arrays; @@ -99,7 +100,7 @@ private static List> getDepartmentsForShops(List shops) { return completedFuture(getDepartmentsForShops(shopList)); }); - public DataLoader> departmentsForShopDataLoader = new DataLoader<>(departmentsForShopsBatchLoader); + public DataLoader> departmentsForShopDataLoader = DataLoaderFactory.newDataLoader(departmentsForShopsBatchLoader); public DataFetcher>> departmentsForShopDataLoaderDataFetcher = environment -> { Shop shop = environment.getSource(); @@ -136,7 +137,7 @@ private static List> getProductsForDepartments(List de return completedFuture(getProductsForDepartments(d)); }); - public DataLoader> productsForDepartmentDataLoader = new DataLoader<>(productsForDepartmentsBatchLoader); + public DataLoader> productsForDepartmentDataLoader = DataLoaderFactory.newDataLoader(productsForDepartmentsBatchLoader); public DataFetcher>> productsForDepartmentDataLoaderDataFetcher = environment -> { Department department = environment.getSource(); diff --git a/src/test/groovy/graphql/execution/instrumentation/dataloader/DataLoaderCompanyProductBackend.java b/src/test/groovy/graphql/execution/instrumentation/dataloader/DataLoaderCompanyProductBackend.java index c49f1abbe4..51d2353bf7 100644 --- a/src/test/groovy/graphql/execution/instrumentation/dataloader/DataLoaderCompanyProductBackend.java +++ b/src/test/groovy/graphql/execution/instrumentation/dataloader/DataLoaderCompanyProductBackend.java @@ -3,9 +3,8 @@ import com.google.common.collect.ImmutableList; import org.dataloader.DataLoader; +import org.dataloader.DataLoaderFactory; -import java.util.ArrayList; -import java.util.Collections; import java.util.List; import java.util.UUID; import java.util.concurrent.CompletableFuture; @@ -27,7 +26,7 @@ public DataLoaderCompanyProductBackend(int companyCount, int projectCount) { mkCompany(projectCount); } - projectsLoader = new DataLoader<>(keys -> getProjectsForCompanies(keys).thenApply(projects -> keys + projectsLoader = DataLoaderFactory.newDataLoader(keys -> getProjectsForCompanies(keys).thenApply(projects -> keys .stream() .map(companyId -> projects.stream() .filter(project -> project.getCompanyId().equals(companyId)) diff --git a/src/test/groovy/graphql/execution/instrumentation/dataloader/DataLoaderDispatcherInstrumentationTest.groovy b/src/test/groovy/graphql/execution/instrumentation/dataloader/DataLoaderDispatcherInstrumentationTest.groovy index 23d00ce90f..ca01f1b3d9 100644 --- a/src/test/groovy/graphql/execution/instrumentation/dataloader/DataLoaderDispatcherInstrumentationTest.groovy +++ b/src/test/groovy/graphql/execution/instrumentation/dataloader/DataLoaderDispatcherInstrumentationTest.groovy @@ -10,12 +10,14 @@ import graphql.execution.ExecutionContext import graphql.execution.ExecutionStrategyParameters import graphql.execution.instrumentation.ChainedInstrumentation import graphql.execution.instrumentation.Instrumentation -import graphql.execution.instrumentation.SimpleInstrumentation +import graphql.execution.instrumentation.InstrumentationState +import graphql.execution.instrumentation.SimplePerformantInstrumentation import graphql.execution.instrumentation.parameters.InstrumentationExecutionParameters import graphql.schema.DataFetcher import org.dataloader.BatchLoader -import org.dataloader.DataLoader +import org.dataloader.DataLoaderFactory import org.dataloader.DataLoaderRegistry +import org.jetbrains.annotations.NotNull import spock.lang.Specification import spock.lang.Unroll @@ -39,7 +41,6 @@ class DataLoaderDispatcherInstrumentationTest extends Specification { } } - def query = """ query { hero { @@ -68,7 +69,7 @@ class DataLoaderDispatcherInstrumentationTest extends Specification { def captureStrategy = new CaptureStrategy() def graphQL = GraphQL.newGraphQL(starWarsSchema).queryExecutionStrategy(captureStrategy) - .instrumentation(new SimpleInstrumentation()) + .instrumentation(new SimplePerformantInstrumentation()) .build() def executionInput = newExecutionInput().query('{ hero { name } }').build() when: @@ -104,7 +105,7 @@ class DataLoaderDispatcherInstrumentationTest extends Specification { super.dispatchAll() } } - def dataLoader = DataLoader.newDataLoader(new BatchLoader() { + def dataLoader = DataLoaderFactory.newDataLoader(new BatchLoader() { @Override CompletionStage load(List keys) { return CompletableFuture.completedFuture(keys) @@ -127,13 +128,15 @@ class DataLoaderDispatcherInstrumentationTest extends Specification { def starWarsWiring = new StarWarsDataLoaderWiring() - DataLoaderRegistry startingDataLoaderRegistry = new DataLoaderRegistry(); + DataLoaderRegistry startingDataLoaderRegistry = new DataLoaderRegistry() def enhancedDataLoaderRegistry = starWarsWiring.newDataLoaderRegistry() def dlInstrumentation = new DataLoaderDispatcherInstrumentation() - def enhancingInstrumentation = new SimpleInstrumentation() { + def enhancingInstrumentation = new SimplePerformantInstrumentation() { + + @NotNull @Override - ExecutionInput instrumentExecutionInput(ExecutionInput executionInput, InstrumentationExecutionParameters parameters) { + ExecutionInput instrumentExecutionInput(ExecutionInput executionInput, InstrumentationExecutionParameters parameters, InstrumentationState state) { assert executionInput.getDataLoaderRegistry() == startingDataLoaderRegistry return executionInput.transform({ builder -> builder.dataLoaderRegistry(enhancedDataLoaderRegistry) }) } @@ -274,7 +277,7 @@ class DataLoaderDispatcherInstrumentationTest extends Specification { BatchLoader batchLoader = { keys -> CompletableFuture.completedFuture(keys) } DataFetcher df = { env -> - def dataLoader = env.getDataLoaderRegistry().computeIfAbsent("key", { key -> DataLoader.newDataLoader(batchLoader) }) + def dataLoader = env.getDataLoaderRegistry().computeIfAbsent("key", { key -> DataLoaderFactory.newDataLoader(batchLoader) }) return dataLoader.load("working as expected") } diff --git a/src/test/groovy/graphql/execution/instrumentation/dataloader/DataLoaderHangingTest.groovy b/src/test/groovy/graphql/execution/instrumentation/dataloader/DataLoaderHangingTest.groovy index 79d783a25c..b76152f75d 100644 --- a/src/test/groovy/graphql/execution/instrumentation/dataloader/DataLoaderHangingTest.groovy +++ b/src/test/groovy/graphql/execution/instrumentation/dataloader/DataLoaderHangingTest.groovy @@ -2,7 +2,6 @@ package graphql.execution.instrumentation.dataloader import com.github.javafaker.Faker import graphql.ExecutionInput -import graphql.ExecutionResult import graphql.GraphQL import graphql.TestUtil import graphql.execution.Async @@ -19,6 +18,7 @@ import graphql.schema.idl.RuntimeWiring import org.apache.commons.lang3.concurrent.BasicThreadFactory import org.dataloader.BatchLoader import org.dataloader.DataLoader +import org.dataloader.DataLoaderFactory import org.dataloader.DataLoaderOptions import org.dataloader.DataLoaderRegistry import spock.lang.Specification @@ -129,7 +129,7 @@ class DataLoaderHangingTest extends Specification { .build() then: "execution shouldn't hang" - List> futures = [] + def futures = Async.ofExpectedSize(NUM_OF_REPS) for (int i = 0; i < NUM_OF_REPS; i++) { DataLoaderRegistry dataLoaderRegistry = mkNewDataLoaderRegistry(executor) @@ -167,7 +167,7 @@ class DataLoaderHangingTest extends Specification { futures.add(result) } // wait for each future to complete and grab the results - Async.each(futures) + futures.await() .whenComplete({ results, error -> if (error) { throw error @@ -178,7 +178,7 @@ class DataLoaderHangingTest extends Specification { } private DataLoaderRegistry mkNewDataLoaderRegistry(executor) { - def dataLoaderAlbums = new DataLoader(new BatchLoader>() { + def dataLoaderAlbums = DataLoaderFactory.newDataLoader(new BatchLoader>() { @Override CompletionStage>> load(List keys) { return CompletableFuture.supplyAsync({ @@ -195,7 +195,7 @@ class DataLoaderHangingTest extends Specification { } }, DataLoaderOptions.newOptions().setMaxBatchSize(5)) - def dataLoaderSongs = new DataLoader(new BatchLoader>() { + def dataLoaderSongs = DataLoaderFactory.newDataLoader(new BatchLoader>() { @Override CompletionStage>> load(List keys) { return CompletableFuture.supplyAsync({ @@ -242,7 +242,7 @@ class DataLoaderHangingTest extends Specification { DataFetcherExceptionHandler customExceptionHandlerThatThrows = new DataFetcherExceptionHandler() { @Override - DataFetcherExceptionHandlerResult onException(DataFetcherExceptionHandlerParameters handlerParameters) { + DataFetcherExceptionHandlerResult onException(DataFetcherExceptionHandlerParameters handlerParameters) { // Retain for test coverage, intentionally using sync version. throw handlerParameters.exception } } @@ -289,7 +289,7 @@ class DataLoaderHangingTest extends Specification { @Override Object get(DataFetchingEnvironment environment) { Product source = environment.getSource() - DataLoaderRegistry dlRegistry = environment.getContext() + DataLoaderRegistry dlRegistry = environment.getGraphQlContext().get("registry") DataLoader personDL = dlRegistry.getDataLoader("person") return personDL.load(source.getSuppliedById()).thenApply({ person -> if (person.id == 0) { @@ -304,7 +304,7 @@ class DataLoaderHangingTest extends Specification { @Override Object get(DataFetchingEnvironment environment) { Person source = environment.getSource() - DataLoaderRegistry dlRegistry = environment.getContext() + DataLoaderRegistry dlRegistry = environment.getGraphQlContext().get("registry") DataLoader companyDL = dlRegistry.getDataLoader("company") return companyDL.load(source.getCompanyId()) } @@ -332,8 +332,8 @@ class DataLoaderHangingTest extends Specification { """ private DataLoaderRegistry buildRegistry() { - DataLoader personDataLoader = new DataLoader<>(personBatchLoader) - DataLoader companyDataLoader = new DataLoader<>(companyBatchLoader) + DataLoader personDataLoader = DataLoaderFactory.newDataLoader(personBatchLoader) + DataLoader companyDataLoader = DataLoaderFactory.newDataLoader(companyBatchLoader) DataLoaderRegistry registry = new DataLoaderRegistry() registry.register("person", personDataLoader) @@ -355,7 +355,7 @@ class DataLoaderHangingTest extends Specification { ExecutionInput executionInput = newExecutionInput() .query(query) - .context(registry) + .graphQLContext(["registry": registry]) .dataLoaderRegistry(registry) .build() diff --git a/src/test/groovy/graphql/execution/instrumentation/dataloader/DataLoaderNodeTest.groovy b/src/test/groovy/graphql/execution/instrumentation/dataloader/DataLoaderNodeTest.groovy index 277b4f5d7c..0bfab06b4f 100644 --- a/src/test/groovy/graphql/execution/instrumentation/dataloader/DataLoaderNodeTest.groovy +++ b/src/test/groovy/graphql/execution/instrumentation/dataloader/DataLoaderNodeTest.groovy @@ -3,6 +3,10 @@ package graphql.execution.instrumentation.dataloader import graphql.ExecutionInput import graphql.ExecutionResult import graphql.GraphQL +import graphql.schema.DataFetcher +import graphql.schema.DataFetchingEnvironment +import graphql.schema.FieldCoordinates +import graphql.schema.GraphQLCodeRegistry import graphql.schema.GraphQLObjectType import graphql.schema.GraphQLSchema import graphql.schema.StaticDataFetcher @@ -64,6 +68,19 @@ class DataLoaderNodeTest extends Specification { } + class NodeDataFetcher implements DataFetcher { + DataLoader loader + + NodeDataFetcher(DataLoader loader) { + this.loader = loader + } + + @Override + Object get(DataFetchingEnvironment environment) throws Exception { + return loader.load(environment.getSource()) + } + } + def "levels of loading"() { List> nodeLoads = [] @@ -78,31 +95,44 @@ class DataLoaderNodeTest extends Specification { return CompletableFuture.completedFuture(childNodes) }) - GraphQLObjectType nodeType = GraphQLObjectType.newObject() - .name("Node") + DataFetcher nodeDataFetcher = new NodeDataFetcher(loader) + + def nodeTypeName = "Node" + def childNodesFieldName = "childNodes" + def queryTypeName = "Query" + def rootFieldName = "root" + + GraphQLObjectType nodeType = GraphQLObjectType + .newObject() + .name(nodeTypeName) .field(newFieldDefinition() - .name("id") - .type(GraphQLInt) - .build()) + .name("id") + .type(GraphQLInt) + .build()) .field(newFieldDefinition() - .name("childNodes") - .type(list(typeRef("Node"))) - .dataFetcher({ environment -> loader.load(environment.getSource()) }) - .build()) + .name(childNodesFieldName) + .type(list(typeRef(nodeTypeName))) + .build()) .build() + def childNodesCoordinates = FieldCoordinates.coordinates(nodeTypeName, childNodesFieldName) + def rootCoordinates = FieldCoordinates.coordinates(queryTypeName, rootFieldName) + GraphQLCodeRegistry codeRegistry = GraphQLCodeRegistry.newCodeRegistry() + .dataFetcher(childNodesCoordinates, nodeDataFetcher) + .dataFetcher(rootCoordinates, new StaticDataFetcher(root)) + .build() GraphQLSchema schema = GraphQLSchema.newSchema() + .codeRegistry(codeRegistry) .query(GraphQLObjectType.newObject() - .name("Query") - .field(newFieldDefinition() - .name("root") - .type(nodeType) - .dataFetcher(new StaticDataFetcher(root)) - .build()) - .build()) + .name(queryTypeName) + .field(newFieldDefinition() + .name(rootFieldName) + .type(nodeType) + .build()) + .build()) .build() - DataLoaderRegistry registry = new DataLoaderRegistry().register("childNodes", loader) + DataLoaderRegistry registry = new DataLoaderRegistry().register(childNodesFieldName, loader) ExecutionResult result = GraphQL.newGraphQL(schema) .instrumentation(new DataLoaderDispatcherInstrumentation()) @@ -146,6 +176,5 @@ class DataLoaderNodeTest extends Specification { // // but currently is this nodeLoads.size() == 3 // WOOT! - } } diff --git a/src/test/groovy/graphql/execution/instrumentation/dataloader/DeepDataFetchers.java b/src/test/groovy/graphql/execution/instrumentation/dataloader/DeepDataFetchers.java index d090425b51..874700f32a 100644 --- a/src/test/groovy/graphql/execution/instrumentation/dataloader/DeepDataFetchers.java +++ b/src/test/groovy/graphql/execution/instrumentation/dataloader/DeepDataFetchers.java @@ -5,68 +5,77 @@ import java.util.function.Supplier; import graphql.schema.DataFetcher; +import graphql.schema.FieldCoordinates; +import graphql.schema.GraphQLCodeRegistry; import graphql.schema.GraphQLFieldDefinition; import graphql.schema.GraphQLObjectType; import graphql.schema.GraphQLSchema; import graphql.schema.GraphQLTypeReference; public class DeepDataFetchers { - private static CompletableFuture supplyAsyncWithSleep(Supplier supplier) { - Supplier sleepSome = sleepSome(supplier); - return CompletableFuture.supplyAsync(sleepSome); - } + private static CompletableFuture supplyAsyncWithSleep(Supplier supplier) { + Supplier sleepSome = sleepSome(supplier); + return CompletableFuture.supplyAsync(sleepSome); + } - private static Supplier sleepSome(Supplier supplier) { - return () -> { - try { - Thread.sleep(10L); - return supplier.get(); - } catch (Exception e) { - throw new RuntimeException(e); - } - }; - } + private static Supplier sleepSome(Supplier supplier) { + return () -> { + try { + Thread.sleep(10L); + return supplier.get(); + } catch (Exception e) { + throw new RuntimeException(e); + } + }; + } - public GraphQLSchema schema() { - DataFetcher>> slowFetcher = environment -> - supplyAsyncWithSleep(HashMap::new); + public GraphQLSchema schema() { + GraphQLFieldDefinition selfField = GraphQLFieldDefinition.newFieldDefinition() + .name("self") + .type(GraphQLTypeReference.typeRef("Query")) + .build(); - GraphQLFieldDefinition selfField = GraphQLFieldDefinition.newFieldDefinition() - .name("self") - .type(GraphQLTypeReference.typeRef("Query")) - .dataFetcher(slowFetcher) - .build(); + GraphQLObjectType query = GraphQLObjectType.newObject() + .name("Query") + .field(selfField) + .build(); - GraphQLObjectType query = GraphQLObjectType.newObject() - .name("Query") - .field(selfField) - .build(); + FieldCoordinates selfCoordinates = FieldCoordinates.coordinates("Query", "self"); + DataFetcher>> slowFetcher = environment -> + supplyAsyncWithSleep(HashMap::new); - return GraphQLSchema.newSchema().query(query).build(); - } + GraphQLCodeRegistry codeRegistry = GraphQLCodeRegistry.newCodeRegistry() + .dataFetcher(selfCoordinates, slowFetcher) + .build(); - public String buildQuery(Integer depth) { - StringBuilder sb = new StringBuilder(); - sb.append("query {"); - for (Integer i = 0; i < depth; i++) { - sb.append("self {"); - } - sb.append("__typename"); - for (Integer i = 0; i < depth; i++) { - sb.append("}"); + return GraphQLSchema.newSchema() + .codeRegistry(codeRegistry) + .query(query) + .build(); } - sb.append("}"); - return sb.toString(); - } + public String buildQuery(Integer depth) { + StringBuilder sb = new StringBuilder(); + sb.append("query {"); + for (Integer i = 0; i < depth; i++) { + sb.append("self {"); + } + sb.append("__typename"); + for (Integer i = 0; i < depth; i++) { + sb.append("}"); + } + sb.append("}"); + + return sb.toString(); + } - public HashMap buildResponse(Integer depth) { - HashMap level = new HashMap<>(); - if (depth == 0) { - level.put("__typename", "Query"); - } else { - level.put("self", buildResponse(depth - 1)); + public HashMap buildResponse(Integer depth) { + HashMap level = new HashMap<>(); + if (depth == 0) { + level.put("__typename", "Query"); + } else { + level.put("self", buildResponse(depth - 1)); + } + return level; } - return level; - } } diff --git a/src/test/groovy/graphql/execution/instrumentation/dataloader/PeopleCompaniesAndProductsDataLoaderTest.groovy b/src/test/groovy/graphql/execution/instrumentation/dataloader/PeopleCompaniesAndProductsDataLoaderTest.groovy index f58a4a9c9d..1b36ec4ecb 100644 --- a/src/test/groovy/graphql/execution/instrumentation/dataloader/PeopleCompaniesAndProductsDataLoaderTest.groovy +++ b/src/test/groovy/graphql/execution/instrumentation/dataloader/PeopleCompaniesAndProductsDataLoaderTest.groovy @@ -105,7 +105,7 @@ class PeopleCompaniesAndProductsDataLoaderTest extends Specification { @Override Object get(DataFetchingEnvironment environment) { Product source = environment.getSource() - DataLoaderRegistry dlRegistry = environment.getContext() + DataLoaderRegistry dlRegistry = environment.getGraphQlContext().get("registry") DataLoader personDL = dlRegistry.getDataLoader("person") return personDL.load(source.getSuppliedById()) } @@ -115,7 +115,7 @@ class PeopleCompaniesAndProductsDataLoaderTest extends Specification { @Override Object get(DataFetchingEnvironment environment) { Product source = environment.getSource() - DataLoaderRegistry dlRegistry = environment.getContext() + DataLoaderRegistry dlRegistry = environment.getGraphQlContext().get("registry") DataLoader personDL = dlRegistry.getDataLoader("person") return personDL.loadMany(source.getMadeByIds()) @@ -126,7 +126,7 @@ class PeopleCompaniesAndProductsDataLoaderTest extends Specification { @Override Object get(DataFetchingEnvironment environment) { Person source = environment.getSource() - DataLoaderRegistry dlRegistry = environment.getContext() + DataLoaderRegistry dlRegistry = environment.getGraphQlContext().get("registry") DataLoader companyDL = dlRegistry.getDataLoader("company") return companyDL.load(source.getCompanyId()) } @@ -191,7 +191,7 @@ class PeopleCompaniesAndProductsDataLoaderTest extends Specification { ExecutionInput executionInput = ExecutionInput.newExecutionInput() .query(query) - .context(registry) + .graphQLContext(["registry": registry]) .dataLoaderRegistry(registry) .build() diff --git a/src/test/groovy/graphql/execution/instrumentation/fieldvalidation/FieldValidationTest.groovy b/src/test/groovy/graphql/execution/instrumentation/fieldvalidation/FieldValidationTest.groovy index 651499e69e..9d473da226 100644 --- a/src/test/groovy/graphql/execution/instrumentation/fieldvalidation/FieldValidationTest.groovy +++ b/src/test/groovy/graphql/execution/instrumentation/fieldvalidation/FieldValidationTest.groovy @@ -11,7 +11,8 @@ import graphql.execution.Execution import graphql.execution.ExecutionId import graphql.execution.ResultPath import graphql.execution.ValueUnboxer -import graphql.execution.instrumentation.SimpleInstrumentation +import graphql.execution.instrumentation.SimplePerformantInstrumentation +import graphql.execution.instrumentation.parameters.InstrumentationCreateStateParameters import spock.lang.Specification import java.util.concurrent.CompletableFuture @@ -154,11 +155,11 @@ class FieldValidationTest extends Specification { SimpleFieldValidation validation = new SimpleFieldValidation() .addRule(ResultPath.parse("/field1"), - { fieldAndArguments, env -> err("Not happy Jan!", env, fieldAndArguments) }) + { fieldAndArguments, env -> err("Not happy Jan!", env, fieldAndArguments) }) .addRule(ResultPath.parse("/field1/informationLink/informationLink/informationString"), - { fieldAndArguments, env -> err("Also not happy Jan!", env, fieldAndArguments) }) + { fieldAndArguments, env -> err("Also not happy Jan!", env, fieldAndArguments) }) .addRule(ResultPath.parse("/does/not/exist"), - { fieldAndArguments, env -> err("Wont happen", env, fieldAndArguments) }) + { fieldAndArguments, env -> err("Wont happen", env, fieldAndArguments) }) when: @@ -203,15 +204,15 @@ class FieldValidationTest extends Specification { SimpleFieldValidation validation = new SimpleFieldValidation() .addRule(ResultPath.parse("/field1/informationString"), - { fieldAndArguments, env -> - def value = fieldAndArguments.getArgumentValue("fmt1") - if (value != "ok") { - return err("Nope : " + value, env, fieldAndArguments) - } else { - return Optional.empty() - } - } - ) + { fieldAndArguments, env -> + def value = fieldAndArguments.getArgumentValue("fmt1") + if (value != "ok") { + return err("Nope : " + value, env, fieldAndArguments) + } else { + return Optional.empty() + } + } + ) when: @@ -249,15 +250,15 @@ class FieldValidationTest extends Specification { SimpleFieldValidation validation = new SimpleFieldValidation() .addRule(ResultPath.parse("/field1/informationString"), - { fieldAndArguments, env -> - String value = fieldAndArguments.getArgumentValue("fmt1") - if (value.contains("alias")) { - return err("Nope : " + value, env, fieldAndArguments) - } else { - return Optional.empty() - } - } - ) + { fieldAndArguments, env -> + String value = fieldAndArguments.getArgumentValue("fmt1") + if (value.contains("alias")) { + return err("Nope : " + value, env, fieldAndArguments) + } else { + return Optional.empty() + } + } + ) when: @@ -308,7 +309,7 @@ class FieldValidationTest extends Specification { def execution = new Execution(strategy, strategy, strategy, instrumentation, ValueUnboxer.DEFAULT) def executionInput = ExecutionInput.newExecutionInput().query(query).variables(variables).build() - execution.execute(document, schema, ExecutionId.generate(), executionInput, SimpleInstrumentation.INSTANCE.createState()) + execution.execute(document, schema, ExecutionId.generate(), executionInput, SimplePerformantInstrumentation.INSTANCE.createState(new InstrumentationCreateStateParameters(schema, executionInput))) } } diff --git a/src/test/groovy/graphql/execution/instrumentation/threadpools/ExecutorInstrumentationTest.groovy b/src/test/groovy/graphql/execution/instrumentation/threadpools/ExecutorInstrumentationTest.groovy index 227d960f89..4a4a453ce6 100644 --- a/src/test/groovy/graphql/execution/instrumentation/threadpools/ExecutorInstrumentationTest.groovy +++ b/src/test/groovy/graphql/execution/instrumentation/threadpools/ExecutorInstrumentationTest.groovy @@ -77,7 +77,7 @@ class ExecutorInstrumentationTest extends Specification { def "can handle a data fetcher that throws exceptions"() { when: DataFetcher df = { env -> throw new RuntimeException("BANG") } - def modifiedDataFetcher = instrumentation.instrumentDataFetcher(df, null) + def modifiedDataFetcher = instrumentation.instrumentDataFetcher(df, null,null) def returnedValue = modifiedDataFetcher.get(null) then: @@ -96,7 +96,7 @@ class ExecutorInstrumentationTest extends Specification { when: DataFetcher df = PropertyDataFetcher.fetching({ o -> "trivial" }) - def modifiedDataFetcher = instrumentation.instrumentDataFetcher(df, null) + def modifiedDataFetcher = instrumentation.instrumentDataFetcher(df, null,null) def returnedValue = modifiedDataFetcher.get(dfEnv("source")) then: @@ -111,7 +111,7 @@ class ExecutorInstrumentationTest extends Specification { instrumentation = build(FetchExecutor, ProcessingExecutor, observer) DataFetcher df = { env -> currentThread().getName() } - def modifiedDataFetcher = instrumentation.instrumentDataFetcher(df, null) + def modifiedDataFetcher = instrumentation.instrumentDataFetcher(df, null, null) def returnedValue = modifiedDataFetcher.get(null) then: @@ -131,7 +131,7 @@ class ExecutorInstrumentationTest extends Specification { instrumentation = build(FetchExecutor, null, observer) DataFetcher df = { env -> currentThread().getName() } - def modifiedDataFetcher = instrumentation.instrumentDataFetcher(df, null) + def modifiedDataFetcher = instrumentation.instrumentDataFetcher(df, null, null) def returnedValue = modifiedDataFetcher.get(null) then: @@ -152,7 +152,7 @@ class ExecutorInstrumentationTest extends Specification { instrumentation = build(null, ProcessingExecutor, observer) DataFetcher df = { env -> currentThread().getName() } - def modifiedDataFetcher = instrumentation.instrumentDataFetcher(df, null) + def modifiedDataFetcher = instrumentation.instrumentDataFetcher(df, null, null) def returnedValue = modifiedDataFetcher.get(null) then: @@ -171,7 +171,7 @@ class ExecutorInstrumentationTest extends Specification { instrumentation = build(FetchExecutor, ProcessingExecutor, observer) DataFetcher df = { env -> CompletableFuture.completedFuture(currentThread().getName()) } - def modifiedDataFetcher = instrumentation.instrumentDataFetcher(df, null) + def modifiedDataFetcher = instrumentation.instrumentDataFetcher(df, null, null) def returnedValue = modifiedDataFetcher.get(null) then: diff --git a/src/test/groovy/graphql/execution/nextgen/BatchedExecutionStrategyTest.groovy b/src/test/groovy/graphql/execution/nextgen/BatchedExecutionStrategyTest.groovy deleted file mode 100644 index 6ba86d013a..0000000000 --- a/src/test/groovy/graphql/execution/nextgen/BatchedExecutionStrategyTest.groovy +++ /dev/null @@ -1,396 +0,0 @@ -package graphql.execution.nextgen - - -import graphql.nextgen.GraphQL -import graphql.schema.DataFetcher -import spock.lang.Specification - -import static graphql.ExecutionInput.newExecutionInput -import static graphql.TestUtil.schema - -class BatchedExecutionStrategyTest extends Specification { - - def "test simple execution"() { - def fooData = [id: "fooId", bar: [id: "barId", name: "someBar"]] - def dataFetchers = [ - Query: [foo: { env -> fooData } as DataFetcher] - ] - def schema = schema(""" - type Query { - foo: Foo - } - type Foo { - id: ID - bar: Bar - } - type Bar { - id: ID - name: String - } - """, dataFetchers) - - - def query = """ - {foo { - id - bar { - id - name - } - }} - """ - - - when: - def graphQL = GraphQL.newGraphQL(schema).executionStrategy(new BatchedExecutionStrategy()).build() - def result = graphQL.execute(newExecutionInput(query)) - then: - result.getData() == [foo: fooData] - } - - 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 = schema(""" - type Query { - foo: [Foo] - } - type Foo { - id: ID - bar: [Bar] - } - type Bar { - id: ID - name: String - } - """, dataFetchers) - - - def query = """ - {foo { - id - bar { - id - name - } - }} - """ - - when: - def graphQL = GraphQL.newGraphQL(schema).executionStrategy(new BatchedExecutionStrategy()).build() - def result = graphQL.execute(newExecutionInput(query)) - then: - result.getData() == [foo: fooData] - } - - 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 = schema(""" - type Query { - foo: [Foo] - } - type Foo { - id: ID - bar: [Bar] - } - type Bar { - id: ID - name: String - } - """, dataFetchers) - - - def query = """ - {foo { - id - bar { - id - name - } - }} - """ - when: - def graphQL = GraphQL.newGraphQL(schema).executionStrategy(new BatchedExecutionStrategy()).build() - def result = graphQL.execute(newExecutionInput(query)) - - then: - result.getData() == [foo: fooData] - - } - - 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 = schema(""" - type Query { - foo: [Foo] - } - type Foo { - id: ID - bar: [Bar] - } - type Bar { - id: ID - name: String - } - """, dataFetchers) - - - def query = """ - {foo { - id - bar { - id - name - } - }} - """ - when: - def graphQL = GraphQL.newGraphQL(schema).executionStrategy(new BatchedExecutionStrategy()).build() - def result = graphQL.execute(newExecutionInput(query)) - then: - result.getData() == [foo: fooData] - } - - 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 = schema(""" - type Query { - foo: [Foo] - } - type Foo { - id: ID - bar: [Bar!] - } - type Bar { - id: ID - name: String - } - """, dataFetchers) - - - def query = """ - {foo { - id - bar { - id - name - } - }} - """ - def expectedFooData = [[id: "fooId1", bar: null], - [id: "fooId2", bar: [[id: "barId3", name: "someBar3"], [id: "barId4", name: "someBar4"]]]] - - when: - def graphQL = GraphQL.newGraphQL(schema).executionStrategy(new BatchedExecutionStrategy()).build() - def result = graphQL.execute(newExecutionInput(query)) - then: - result.getData() == [foo: expectedFooData] - } - - 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 = schema(""" - type Query { - foo: [Foo] - } - type Foo { - id: ID - bar: [Bar!]! - } - type Bar { - id: ID - name: String - } - """, dataFetchers) - - - def query = """ - {foo { - id - bar { - id - name - } - }} - """ - - def expectedFooData = [null, - [id: "fooId2", bar: [[id: "barId3", name: "someBar3"], [id: "barId4", name: "someBar4"]]]] - - when: - def graphQL = GraphQL.newGraphQL(schema).executionStrategy(new BatchedExecutionStrategy()).build() - def result = graphQL.execute(newExecutionInput(query)) - then: - result.getData() == [foo: expectedFooData] - } - - 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 = schema(""" - type Query { - foo: [Foo!]! - } - type Foo { - id: ID - bar: [Bar!]! - } - type Bar { - id: ID - name: String - } - """, dataFetchers) - - - def query = """ - {foo { - id - bar { - id - name - } - }} - """ - when: - def graphQL = GraphQL.newGraphQL(schema).executionStrategy(new BatchedExecutionStrategy()).build() - def result = graphQL.execute(newExecutionInput(query)) - then: - result.getData() == null - } - - def "test list"() { - def fooData = [[id: "fooId1"], [id: "fooId2"], [id: "fooId3"]] - def dataFetchers = [ - Query: [foo: { env -> fooData } as DataFetcher] - ] - def schema = schema(""" - type Query { - foo: [Foo] - } - type Foo { - id: ID - } - """, dataFetchers) - - - def query = """ - {foo { - id - }} - """ - - when: - def graphQL = GraphQL.newGraphQL(schema).executionStrategy(new BatchedExecutionStrategy()).build() - def result = graphQL.execute(newExecutionInput(query)) - then: - result.getData() == [foo: fooData] - } - - def "test list in lists "() { - def catsBatchSize = 0 - def catsCallCount = 0 - def idsBatchSize = 0 - def idsCallCount = 0 - - def catsDataFetcher = { env -> - catsCallCount++ - catsBatchSize = env.getSource().size() - return [["cat1", "cat2"], null, ["cat3", "cat4", "cat5"]] - } as BatchedDataFetcher - - def idDataFetcher = { env -> - idsCallCount++ - idsBatchSize = env.getSource().size() - return ["catId1", "catId2", "catId3", "catId4", "catId5"] - } as BatchedDataFetcher - - def friendsData = ["friend1", "friend2", "friend3"] - def dataFetchers = [ - Query : [friends: { env -> friendsData } as DataFetcher], - Person: [cats: catsDataFetcher], - Cat : [id: idDataFetcher] - ] - def schema = schema(""" - type Query { - friends: [Person] - } - type Person { - cats: [Cat] - } - type Cat { - id: ID - } - """, dataFetchers) - - - def query = """ - {friends { - cats { - id - } - }} - """ - - when: - def graphQL = GraphQL.newGraphQL(schema).executionStrategy(new BatchedExecutionStrategy()).build() - def result = graphQL.execute(newExecutionInput(query)) - then: - result.getData() == [friends: [[cats: [[id: "catId1"], [id: "catId2"]]], [cats: null], [cats: [[id: "catId3"], [id: "catId4"], [id: "catId5"]]]]] - catsCallCount == 1 - idsCallCount == 1 - catsBatchSize == 3 - idsBatchSize == 5 - } - - 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 = schema(""" - type Query { - foo: [Foo] - } - type Foo { - id: ID - } - """, dataFetchers) - - - def query = """ - {foo { - id - }} - """ - - when: - def graphQL = GraphQL.newGraphQL(schema).executionStrategy(new BatchedExecutionStrategy()).build() - def result = graphQL.execute(newExecutionInput(query)) - - then: - result.getData() == [foo: fooData] - } -} - diff --git a/src/test/groovy/graphql/execution/nextgen/DefaultExecutionStrategyTest.groovy b/src/test/groovy/graphql/execution/nextgen/DefaultExecutionStrategyTest.groovy deleted file mode 100644 index 24bd070530..0000000000 --- a/src/test/groovy/graphql/execution/nextgen/DefaultExecutionStrategyTest.groovy +++ /dev/null @@ -1,549 +0,0 @@ -package graphql.execution.nextgen - -import graphql.ExceptionWhileDataFetching -import graphql.nextgen.GraphQL -import graphql.schema.DataFetcher -import graphql.schema.DataFetchingEnvironment -import spock.lang.Specification - -import java.util.concurrent.CompletableFuture - -import static graphql.ExecutionInput.newExecutionInput -import static graphql.TestUtil.schema -import static graphql.execution.DataFetcherResult.newResult - -class DefaultExecutionStrategyTest extends Specification { - - def "test simple execution with one scalar field"() { - def fooData = "hello" - def dataFetchers = [ - Query: [foo: { env -> fooData } as DataFetcher] - ] - def schema = schema(""" - type Query { - foo: String - } - """, dataFetchers) - - - def query = """ - {foo} - """ - - - when: - def graphQL = GraphQL.newGraphQL(schema).executionStrategy(new DefaultExecutionStrategy()).build() - def result = graphQL.execute(newExecutionInput().query(query)) - - then: - result.getData() == [foo: fooData] - - } - - def "test simple execution"() { - def fooData = [id: "fooId", bar: [id: "barId", name: "someBar"]] - def dataFetchers = [ - Query: [foo: { env -> fooData } as DataFetcher] - ] - def schema = schema(""" - type Query { - foo: Foo - } - type Foo { - id: ID - bar: Bar - } - type Bar { - id: ID - name: String - } - """, dataFetchers) - - - def query = """ - {foo { - id - bar { - id - name - } - }} - """ - - - when: - def graphQL = GraphQL.newGraphQL(schema).executionStrategy(new DefaultExecutionStrategy()).build() - def result = graphQL.execute(newExecutionInput().query(query)) - - then: - result.getData() == [foo: fooData] - - } - - @SuppressWarnings("GroovyAssignabilityCheck") - def "fields are resolved in depth in parallel"() { - - List cfs = [] - - def fooResolver = { env -> - println env.getField() - def result = new CompletableFuture() - cfs << result - result - } as DataFetcher - def idResolver1 = Mock(DataFetcher) - def idResolver2 = Mock(DataFetcher) - def idResolver3 = Mock(DataFetcher) - def dataFetchers = [ - Query: [foo: fooResolver], - Foo : [id1: idResolver1, id2: idResolver2, id3: idResolver3] - ] - def schema = schema(""" - type Query { - foo: Foo - } - type Foo { - id1: ID - id2: ID - id3: ID - } - """, dataFetchers) - - - def query = """ - { - f1: foo { id1 } - f2: foo { id2 } - f3: foo { id3 } - } - """ - - def cfId1 = new CompletableFuture() - def cfId2 = new CompletableFuture() - def cfId3 = new CompletableFuture() - - when: - def graphQL = GraphQL.newGraphQL(schema).executionStrategy(new DefaultExecutionStrategy()).build() - // - // I think this is a dangerous test - It dispatches the query but never joins on the result - // so its expecting the DFs to be called by never resolved properly. ?? - graphQL.executeAsync(newExecutionInput().query(query)) - - then: - cfs.size() == 3 - 0 * idResolver1.get(_) - 0 * idResolver1.get(_) - 0 * idResolver1.get(_) - - when: - cfs[1].complete(new Object()) - then: - 0 * idResolver1.get(_) - 1 * idResolver2.get(_) >> cfId2 - 0 * idResolver3.get(_) - - when: - cfs[2].complete(new Object()) - then: - 0 * idResolver1.get(_) - 0 * idResolver2.get(_) - 1 * idResolver3.get(_) >> cfId3 - - - when: - cfs[0].complete(new Object()) - then: - 1 * idResolver1.get(_) >> cfId1 - 0 * idResolver2.get(_) - 0 * idResolver3.get(_) - - } - - 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 = schema(""" - type Query { - foo: [Foo] - } - type Foo { - id: ID - bar: [Bar] - } - type Bar { - id: ID - name: String - } - """, dataFetchers) - - - def query = """ - {foo { - id - bar { - id - name - } - }} - """ - - when: - def graphQL = GraphQL.newGraphQL(schema).executionStrategy(new DefaultExecutionStrategy()).build() - def result = graphQL.execute(newExecutionInput().query(query)) - - then: - result.getData() == [foo: fooData] - } - - 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 = schema(""" - type Query { - foo: [Foo] - } - type Foo { - id: ID - bar: [Bar] - } - type Bar { - id: ID - name: String - } - """, dataFetchers) - - - def query = """ - {foo { - id - bar { - id - name - } - }} - """ - - when: - def graphQL = GraphQL.newGraphQL(schema).executionStrategy(new DefaultExecutionStrategy()).build() - def result = graphQL.execute(newExecutionInput().query(query)) - - then: - result.getData() == [foo: fooData] - - } - - 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 = schema(""" - type Query { - foo: [Foo] - } - type Foo { - id: ID - bar: [Bar] - } - type Bar { - id: ID - name: String - } - """, dataFetchers) - - - def query = """ - {foo { - id - bar { - id - name - } - }} - """ - - when: - def graphQL = GraphQL.newGraphQL(schema).executionStrategy(new DefaultExecutionStrategy()).build() - def result = graphQL.execute(newExecutionInput().query(query)) - - then: - result.getData() == [foo: fooData] - - } - - 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 = schema(""" - type Query { - foo: [Foo] - } - type Foo { - id: ID - bar: [Bar!] - } - type Bar { - id: ID - name: String - } - """, dataFetchers) - - - def query = """ - {foo { - id - bar { - id - name - } - }} - """ - - def expectedFooData = [[id: "fooId1", bar: null], - [id: "fooId2", bar: [[id: "barId3", name: "someBar3"], [id: "barId4", name: "someBar4"]]]] - - - when: - def graphQL = GraphQL.newGraphQL(schema).executionStrategy(new DefaultExecutionStrategy()).build() - def result = graphQL.execute(newExecutionInput().query(query)) - - then: - result.getData() == [foo: expectedFooData] - - } - - 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 = schema(""" - type Query { - foo: [Foo] - } - type Foo { - id: ID - bar: [Bar!]! - } - type Bar { - id: ID - name: String - } - """, dataFetchers) - - - def query = """ - {foo { - id - bar { - id - name - } - }} - """ - - def expectedFooData = [null, - [id: "fooId2", bar: [[id: "barId3", name: "someBar3"], [id: "barId4", name: "someBar4"]]]] - - when: - def graphQL = GraphQL.newGraphQL(schema).build() - def result = graphQL.execute(newExecutionInput().query(query)) - - then: - result.getData() == [foo: expectedFooData] - } - - 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 = schema(""" - type Query { - foo: [Foo!]! - } - type Foo { - id: ID - bar: [Bar!]! - } - type Bar { - id: ID - name: String - } - """, dataFetchers) - - - def query = """ - {foo { - id - bar { - id - name - } - }} - """ - - when: - def graphQL = GraphQL.newGraphQL(schema).build() - def result = graphQL.execute(newExecutionInput().query(query)) - - then: - result.getData() == null - result.getErrors().size() > 0 - } - - def "test list"() { - def fooData = [[id: "fooId1"], [id: "fooId2"], [id: "fooId3"]] - def dataFetchers = [ - Query: [foo: { env -> fooData } as DataFetcher] - ] - def schema = schema(""" - type Query { - foo: [Foo] - } - type Foo { - id: ID - } - """, dataFetchers) - - - def query = """ - {foo { - id - }} - """ - - when: - def graphQL = GraphQL.newGraphQL(schema).build() - def result = graphQL.execute(newExecutionInput().query(query)) - - then: - result.getData() == [foo: fooData] - } - - 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 = schema(""" - type Query { - foo: [Foo] - } - type Foo { - bar: [Bar] - } - type Bar { - id: ID - } - """, dataFetchers) - - - def query = """ - {foo { - bar { - id - } - }} - """ - when: - def graphQL = GraphQL.newGraphQL(schema).build() - def result = graphQL.execute(newExecutionInput().query(query)) - - then: - result.getData() == [foo: fooData] - } - - 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 = schema(""" - type Query { - foo: [Foo] - } - type Foo { - id: ID - } - """, dataFetchers) - - - def query = """ - {foo { - id - }} - """ - - when: - def graphQL = GraphQL.newGraphQL(schema).build() - def result = graphQL.execute(newExecutionInput().query(query)) - - then: - result.getData() == [foo: fooData] - } - - - def "DataFetcherResult is respected with errors"() { - - def fooData = [[id: "fooId1"], null, [id: "fooId3"]] - def dataFetchers = [ - Query: [ - foo: { env -> - newResult().data(fooData) - .error(mkError(env)) - .build() - } as DataFetcher], - Foo : [ - id: { env -> - def id = env.source[env.getField().getName()] - newResult().data(id) - .error(mkError(env)) - .build() - } as DataFetcher - ] - ] - def schema = schema(''' - type Query { - foo: [Foo] - } - type Foo { - id: ID - } - ''', dataFetchers) - - - def query = ''' - { - foo { - id - } - } - ''' - - when: - def graphQL = GraphQL.newGraphQL(schema).build() - def result = graphQL.execute(newExecutionInput().query(query)) - - then: - result.errors.size() == 3 - result.data == [foo: fooData] - } - - private static ExceptionWhileDataFetching mkError(DataFetchingEnvironment env) { - def rte = new RuntimeException("Bang on " + env.getField().getName()) - new ExceptionWhileDataFetching(env.executionStepInfo.getPath(), rte, env.getField().sourceLocation) - } -} diff --git a/src/test/groovy/graphql/execution/nextgen/result/ExecutionResultNodeTest.groovy b/src/test/groovy/graphql/execution/nextgen/result/ExecutionResultNodeTest.groovy deleted file mode 100644 index a2ad92b1a2..0000000000 --- a/src/test/groovy/graphql/execution/nextgen/result/ExecutionResultNodeTest.groovy +++ /dev/null @@ -1,90 +0,0 @@ -package graphql.execution.nextgen.result - -import graphql.Scalars -import spock.lang.Shared -import spock.lang.Specification -import spock.lang.Unroll - -import static graphql.GraphqlErrorBuilder.newError -import static graphql.execution.ExecutionStepInfo.newExecutionStepInfo - -class ExecutionResultNodeTest extends Specification { - - @Shared - def startingErrors = [newError().message("Starting").build()] - - @Shared - def startingExecutionStepInfo = newExecutionStepInfo().type(Scalars.GraphQLString).build() - @Shared - def startingResolveValue = ResolvedValue.newResolvedValue().completedValue("start").build(); - - @Shared - def startingChildren = [new LeafExecutionResultNode(startingExecutionStepInfo, startingResolveValue, null)] - - @Unroll - def "construction of objects with new errors works"() { - - given: - def expectedErrors = [newError().message("Expected").build()] - - expect: - ExecutionResultNode nodeUnderTest = node - def newNode = nodeUnderTest.withNewErrors(expectedErrors) - newNode != nodeUnderTest - newNode.getErrors() == expectedErrors - - where: - - node | _ - new RootExecutionResultNode([], startingErrors) | _ - new ObjectExecutionResultNode(startingExecutionStepInfo, startingResolveValue, startingChildren, startingErrors) | _ - new ListExecutionResultNode(startingExecutionStepInfo, startingResolveValue, startingChildren, startingErrors) | _ - new LeafExecutionResultNode(startingExecutionStepInfo, startingResolveValue, null, startingErrors) | _ - } - - @Unroll - def "construction of objects with new esi works"() { - - given: - def newEsi = newExecutionStepInfo().type(Scalars.GraphQLString).build() - - expect: - ExecutionResultNode nodeUnderTest = node - def newNode = nodeUnderTest.withNewExecutionStepInfo(newEsi) - newNode != nodeUnderTest - newNode.getExecutionStepInfo() == newEsi - - - where: - - node | _ - new ObjectExecutionResultNode(startingExecutionStepInfo, startingResolveValue, startingChildren, startingErrors) | _ - new ListExecutionResultNode(startingExecutionStepInfo, startingResolveValue, startingChildren, startingErrors) | _ - new LeafExecutionResultNode(startingExecutionStepInfo, startingResolveValue, null, startingErrors) | _ - } - - @Unroll - def "construction of objects with new children works"() { - - given: - def newChildren = [ - new LeafExecutionResultNode(startingExecutionStepInfo, startingResolveValue, null), - new LeafExecutionResultNode(startingExecutionStepInfo, startingResolveValue, null), - ] - - expect: - ExecutionResultNode nodeUnderTest = node - node.getChildren().size() == 1 - def newNode = nodeUnderTest.withNewChildren(newChildren) - newNode != nodeUnderTest - newNode.getChildren().size() == 2 - - - where: - - node | _ - new RootExecutionResultNode(startingChildren, startingErrors) | _ - new ObjectExecutionResultNode(startingExecutionStepInfo, startingResolveValue, startingChildren, startingErrors) | _ - new ListExecutionResultNode(startingExecutionStepInfo, startingResolveValue, startingChildren, startingErrors) | _ - } -} diff --git a/src/test/groovy/graphql/execution/nextgen/result/ExecutionResultNodeTestUtils.groovy b/src/test/groovy/graphql/execution/nextgen/result/ExecutionResultNodeTestUtils.groovy deleted file mode 100644 index 4aeac436dd..0000000000 --- a/src/test/groovy/graphql/execution/nextgen/result/ExecutionResultNodeTestUtils.groovy +++ /dev/null @@ -1,19 +0,0 @@ -package graphql.execution.nextgen.result - -import graphql.Scalars -import graphql.execution.ExecutionStepInfo - -import static graphql.execution.ExecutionStepInfo.newExecutionStepInfo - -class ExecutionResultNodeTestUtils { - - - static ResolvedValue resolvedValue(Object value) { - return ResolvedValue.newResolvedValue().completedValue(value).build() - } - - static ExecutionStepInfo esi() { - return newExecutionStepInfo().type(Scalars.GraphQLString).build() - } - -} diff --git a/src/test/groovy/graphql/execution/nextgen/result/ResultNodeAdapterTest.groovy b/src/test/groovy/graphql/execution/nextgen/result/ResultNodeAdapterTest.groovy deleted file mode 100644 index bf0b3e3a1a..0000000000 --- a/src/test/groovy/graphql/execution/nextgen/result/ResultNodeAdapterTest.groovy +++ /dev/null @@ -1,36 +0,0 @@ -package graphql.execution.nextgen.result - -import graphql.AssertException -import graphql.util.NodeLocation -import spock.lang.Specification - -import static graphql.execution.nextgen.result.ExecutionResultNodeTestUtils.esi -import static graphql.execution.nextgen.result.ExecutionResultNodeTestUtils.resolvedValue - -class ResultNodeAdapterTest extends Specification { - - def parentNode = new ObjectExecutionResultNode(null, null, [ - new LeafExecutionResultNode(esi(), resolvedValue("v1"), null), - new LeafExecutionResultNode(esi(), resolvedValue("v2"), null), - new LeafExecutionResultNode(esi(), resolvedValue("v3"), null), - ]) - - def "can remove a child from a node"() { - when: - ResultNodeAdapter.RESULT_NODE_ADAPTER.removeChild(parentNode, new NodeLocation(null, -1)) - then: - thrown(AssertException) - - when: - ResultNodeAdapter.RESULT_NODE_ADAPTER.removeChild(parentNode, new NodeLocation(null, -3)) - then: - thrown(AssertException) - - when: - def newNode = ResultNodeAdapter.RESULT_NODE_ADAPTER.removeChild(parentNode, new NodeLocation(null, 1)) - then: - newNode.children.size() == 2 - newNode.children[0].getResolvedValue().getCompletedValue() == "v1" - newNode.children[1].getResolvedValue().getCompletedValue() == "v3" - } -} diff --git a/src/test/groovy/graphql/execution/nextgen/result/ResultNodesUtilTest.groovy b/src/test/groovy/graphql/execution/nextgen/result/ResultNodesUtilTest.groovy deleted file mode 100644 index 9f3042d02e..0000000000 --- a/src/test/groovy/graphql/execution/nextgen/result/ResultNodesUtilTest.groovy +++ /dev/null @@ -1,33 +0,0 @@ -package graphql.execution.nextgen.result - -import graphql.SerializationError -import graphql.execution.ExecutionStepInfo -import graphql.execution.MergedField -import graphql.execution.ResultPath -import graphql.schema.CoercingSerializeException -import spock.lang.Specification - -class ResultNodesUtilTest extends Specification { - - def "convert errors for null values"() { - given: - def error = new SerializationError(ResultPath.rootPath(), new CoercingSerializeException()) - ExecutionStepInfo executionStepInfo = Mock(ExecutionStepInfo) - MergedField mergedField = Mock(MergedField) - mergedField.getResultKey() >> "foo" - executionStepInfo.getField() >> mergedField - ResolvedValue resolvedValue = ResolvedValue.newResolvedValue() - .completedValue(null) - .nullValue(true) - .errors([error]) - .build() - - LeafExecutionResultNode leafExecutionResultNode = new LeafExecutionResultNode(executionStepInfo, resolvedValue, null) - ExecutionResultNode executionResultNode = new RootExecutionResultNode([leafExecutionResultNode]) - - when: - def executionResult = ResultNodesUtil.toExecutionResult(executionResultNode) - then: - executionResult.errors.size() == 1 - } -} diff --git a/src/test/groovy/graphql/execution/preparsed/PreparsedDocumentProviderTest.groovy b/src/test/groovy/graphql/execution/preparsed/PreparsedDocumentProviderTest.groovy index 5bc311c843..104d00ea28 100644 --- a/src/test/groovy/graphql/execution/preparsed/PreparsedDocumentProviderTest.groovy +++ b/src/test/groovy/graphql/execution/preparsed/PreparsedDocumentProviderTest.groovy @@ -6,8 +6,9 @@ import graphql.GraphQL import graphql.StarWarsSchema import graphql.execution.AsyncExecutionStrategy import graphql.execution.instrumentation.InstrumentationContext -import graphql.execution.instrumentation.SimpleInstrumentation +import graphql.execution.instrumentation.InstrumentationState import graphql.execution.instrumentation.LegacyTestingInstrumentation +import graphql.execution.instrumentation.SimplePerformantInstrumentation import graphql.execution.instrumentation.parameters.InstrumentationExecutionParameters import graphql.language.Document import graphql.parser.Parser @@ -16,6 +17,7 @@ import spock.lang.Specification import java.util.function.Function import static graphql.ExecutionInput.newExecutionInput +import static graphql.execution.instrumentation.SimpleInstrumentationContext.noOp class PreparsedDocumentProviderTest extends Specification { @@ -110,14 +112,14 @@ class PreparsedDocumentProviderTest extends Specification { .instrumentation(instrumentation) .preparsedDocumentProvider(preparsedCache) .build() - .execute(ExecutionInput.newExecutionInput().query(query).build()).data + .execute(newExecutionInput().query(query).build()).data def data2 = GraphQL.newGraphQL(StarWarsSchema.starWarsSchema) .queryExecutionStrategy(strategy) .instrumentation(instrumentationPreparsed) .preparsedDocumentProvider(preparsedCache) .build() - .execute(ExecutionInput.newExecutionInput().query(query).build()).data + .execute(newExecutionInput().query(query).build()).data then: @@ -145,12 +147,12 @@ class PreparsedDocumentProviderTest extends Specification { def result1 = GraphQL.newGraphQL(StarWarsSchema.starWarsSchema) .preparsedDocumentProvider(preparsedCache) .build() - .execute(ExecutionInput.newExecutionInput().query(query).build()) + .execute(newExecutionInput().query(query).build()) def result2 = GraphQL.newGraphQL(StarWarsSchema.starWarsSchema) .preparsedDocumentProvider(preparsedCache) .build() - .execute(ExecutionInput.newExecutionInput().query(query).build()) + .execute(newExecutionInput().query(query).build()) then: "Both the first and the second result should give the same validation error" result1.errors.size() == 1 @@ -161,13 +163,13 @@ class PreparsedDocumentProviderTest extends Specification { result1.errors[0].errorType == result2.errors[0].errorType } - class InputCapturingInstrumentation extends SimpleInstrumentation { + class InputCapturingInstrumentation extends SimplePerformantInstrumentation { ExecutionInput capturedInput @Override - InstrumentationContext beginParse(InstrumentationExecutionParameters parameters) { + InstrumentationContext beginParse(InstrumentationExecutionParameters parameters, InstrumentationState state) { capturedInput = parameters.getExecutionInput() - return super.beginParse(parameters) + return noOp() } } @@ -206,14 +208,14 @@ class PreparsedDocumentProviderTest extends Specification { .preparsedDocumentProvider(documentProvider) .instrumentation(instrumentationA) .build() - .execute(ExecutionInput.newExecutionInput().query("#A").build()) + .execute(newExecutionInput().query("#A").build()) def instrumentationB = new InputCapturingInstrumentation() def resultB = GraphQL.newGraphQL(StarWarsSchema.starWarsSchema) .preparsedDocumentProvider(documentProvider) .instrumentation(instrumentationB) .build() - .execute(ExecutionInput.newExecutionInput().query("#B").build()) + .execute(newExecutionInput().query("#B").build()) expect: diff --git a/src/test/groovy/graphql/i18n/I18nTest.groovy b/src/test/groovy/graphql/i18n/I18nTest.groovy index 7abe1346a6..84165264e7 100644 --- a/src/test/groovy/graphql/i18n/I18nTest.groovy +++ b/src/test/groovy/graphql/i18n/I18nTest.groovy @@ -1,6 +1,8 @@ package graphql.i18n import graphql.AssertException +import graphql.ExecutionInput +import graphql.TestUtil import graphql.i18n.I18n.BundleType import spock.lang.Specification @@ -14,6 +16,43 @@ class I18nTest extends Specification { thrown(AssertException) } + def "missing resource bundles default to a base version"() { + // see https://saimana.com/list-of-country-locale-code/ + + def expected = "Validation error ({0}) : Type '{1}' definition is not executable" + + when: + def i18n = I18n.i18n(BundleType.Validation, Locale.ENGLISH) + def msg = i18n.msg("ExecutableDefinitions.notExecutableType") + + then: + msg == expected + + when: + i18n = I18n.i18n(BundleType.Validation, Locale.CHINESE) + msg = i18n.msg("ExecutableDefinitions.notExecutableType") + then: + msg == expected + + when: + i18n = I18n.i18n(BundleType.Validation, new Locale("en", "IN")) // India + msg = i18n.msg("ExecutableDefinitions.notExecutableType") + then: + msg == expected + + when: + i18n = I18n.i18n(BundleType.Validation, new Locale("en", "FJ")) // Fiji + msg = i18n.msg("ExecutableDefinitions.notExecutableType") + then: + msg == expected + + when: + i18n = I18n.i18n(BundleType.Validation, new Locale("")) // Nothing + msg = i18n.msg("ExecutableDefinitions.notExecutableType") + then: + msg == expected + } + def "all enums have resources and decent shapes"() { when: def bundleTypes = BundleType.values() @@ -26,6 +65,67 @@ class I18nTest extends Specification { } } + def "A non-default bundle can be read"() { + def i18n = I18n.i18n(BundleType.Validation, Locale.GERMAN) + when: + def message = i18n.msg("ExecutableDefinitions.notExecutableType") + then: + message == "Validierungsfehler ({0}) : Type definition '{1}' ist nicht ausführbar" + } + + def "integration test of valid messages"() { + def sdl = """ + type Query { + field(arg : Int) : Subselection + } + + type Subselection { + name : String + } + """ + def graphQL = TestUtil.graphQL(sdl).build() + + + when: + def locale = new Locale("en", "IN") + def ei = ExecutionInput.newExecutionInput().query("query missingSubselectionQ { field(arg : 1) }") + .locale(locale) + .build() + def er = graphQL.execute(ei) + then: + !er.errors.isEmpty() + er.errors[0].message == "Validation error (SubselectionRequired@[field]) : Subselection required for type 'Subselection' of field 'field'" + + when: + locale = Locale.GERMANY + ei = ExecutionInput.newExecutionInput().query("query missingSubselectionQ { field(arg : 1) }") + .locale(locale) + .build() + er = graphQL.execute(ei) + then: + !er.errors.isEmpty() + er.errors[0].message == "Validierungsfehler (SubselectionRequired@[field]) : Unterauswahl erforderlich für Typ 'Subselection' des Feldes 'field'" + + when: + locale = Locale.getDefault() + ei = ExecutionInput.newExecutionInput().query("query missingSubselectionQ { field(arg : 1) }") + .locale(locale) + .build() + er = graphQL.execute(ei) + then: + !er.errors.isEmpty() + er.errors[0].message == "Validation error (SubselectionRequired@[field]) : Subselection required for type 'Subselection' of field 'field'" + + when: + // no locale - it should default + ei = ExecutionInput.newExecutionInput().query("query missingSubselectionQ { field(arg : 1) }") + .build() + er = graphQL.execute(ei) + then: + !er.errors.isEmpty() + er.errors[0].message == "Validation error (SubselectionRequired@[field]) : Subselection required for type 'Subselection' of field 'field'" + } + static def assertBundleStaticShape(ResourceBundle bundle) { def enumeration = bundle.getKeys() while (enumeration.hasMoreElements()) { diff --git a/src/test/groovy/graphql/introspection/IntrospectionResultToSchemaTest.groovy b/src/test/groovy/graphql/introspection/IntrospectionResultToSchemaTest.groovy index f140b65a15..9f2346df4f 100644 --- a/src/test/groovy/graphql/introspection/IntrospectionResultToSchemaTest.groovy +++ b/src/test/groovy/graphql/introspection/IntrospectionResultToSchemaTest.groovy @@ -3,7 +3,7 @@ package graphql.introspection import com.fasterxml.jackson.databind.ObjectMapper import graphql.Assert import graphql.ExecutionInput -import graphql.ExecutionResultImpl +import graphql.ExecutionResult import graphql.GraphQL import graphql.TestUtil import graphql.language.Document @@ -700,7 +700,7 @@ input InputType { def "create schema fail"() { given: - def failResult = ExecutionResultImpl.newExecutionResult().build() + def failResult = ExecutionResult.newExecutionResult().build() when: Document document = introspectionResultToSchema.createSchemaDefinition(failResult) diff --git a/src/test/groovy/graphql/introspection/IntrospectionTest.groovy b/src/test/groovy/graphql/introspection/IntrospectionTest.groovy index 5dd073e629..712ac8048c 100644 --- a/src/test/groovy/graphql/introspection/IntrospectionTest.groovy +++ b/src/test/groovy/graphql/introspection/IntrospectionTest.groovy @@ -11,6 +11,13 @@ import spock.lang.See import spock.lang.Specification import static graphql.GraphQL.newGraphQL +import static graphql.Scalars.GraphQLString +import static graphql.schema.GraphQLArgument.newArgument +import static graphql.schema.GraphQLFieldDefinition.newFieldDefinition +import static graphql.schema.GraphQLInputObjectField.newInputObjectField +import static graphql.schema.GraphQLInputObjectType.newInputObject +import static graphql.schema.GraphQLObjectType.newObject +import static graphql.schema.GraphQLSchema.newSchema class IntrospectionTest extends Specification { @@ -143,7 +150,7 @@ class IntrospectionTest extends Specification { then: executionResult.errors.isEmpty() - def directives = executionResult.data.getAt("__schema").getAt("directives") as List + def directives = executionResult.data["__schema"]["directives"] as List def geoPolygonType = directives.find { it['name'] == 'repeatableDirective' } geoPolygonType["isRepeatable"] == true } @@ -353,4 +360,266 @@ class IntrospectionTest extends Specification { then: queryTypeFields == [[name: "inA"], [name: "inB"], [name: "inC"]] } + + def "test introspection for #296 with map"() { + + def graphql = newGraphQL(newSchema() + .query(newObject() + .name("Query") + .field(newFieldDefinition() + .name("field") + .type(GraphQLString) + .argument(newArgument() + .name("argument") + .type(newInputObject() + .name("InputObjectType") + .field(newInputObjectField() + .name("inputField") + .type(GraphQLString)) + .build()) + .defaultValueProgrammatic([inputField: 'value1']) + ) + ) + ) + .build() + ).build() + + def query = '{ __type(name: "Query") { fields { args { defaultValue } } } }' + + expect: + // converts the default object value to AST, then graphql pretty prints that as the value + graphql.execute(query).data == + [__type: [fields: [[args: [[defaultValue: '{inputField : "value1"}']]]]]] + } + + class FooBar { + final String inputField = "foo" + final String bar = "bar" + + String getInputField() { + return inputField + } + + String getBar() { + return bar + } + } + + def "test introspection for #296 with some object"() { + + def graphql = newGraphQL(newSchema() + .query(newObject() + .name("Query") + .field(newFieldDefinition() + .name("field") + .type(GraphQLString) + .argument(newArgument() + .name("argument") + .type(newInputObject() + .name("InputObjectType") + .field(newInputObjectField() + .name("inputField") + .type(GraphQLString)) + .build()) + .defaultValue(new FooBar()) // Retain for test coverage. There is no alternative method that sets an internal value. + ) + ) + ) + .build() + ).build() + + def query = '{ __type(name: "Query") { fields { args { defaultValue } } } }' + + expect: + // converts the default object value to AST, then graphql pretty prints that as the value + graphql.execute(query).data == + [__type: [fields: [[args: [[defaultValue: '{inputField : "foo"}']]]]]] + } + + def "test AST printed introspection query is equivalent to original string"() { + when: + def oldIntrospectionQuery = "\n" + + " query IntrospectionQuery {\n" + + " __schema {\n" + + " queryType { name }\n" + + " mutationType { name }\n" + + " subscriptionType { name }\n" + + " types {\n" + + " ...FullType\n" + + " }\n" + + " directives {\n" + + " name\n" + + " description\n" + + " locations\n" + + " args(includeDeprecated: true) {\n" + + " ...InputValue\n" + + " }\n" + + " isRepeatable\n" + + " }\n" + + " }\n" + + " }\n" + + "\n" + + " fragment FullType on __Type {\n" + + " kind\n" + + " name\n" + + " description\n" + + " fields(includeDeprecated: true) {\n" + + " name\n" + + " description\n" + + " args(includeDeprecated: true) {\n" + + " ...InputValue\n" + + " }\n" + + " type {\n" + + " ...TypeRef\n" + + " }\n" + + " isDeprecated\n" + + " deprecationReason\n" + + " }\n" + + " inputFields(includeDeprecated: true) {\n" + + " ...InputValue\n" + + " }\n" + + " interfaces {\n" + + " ...TypeRef\n" + + " }\n" + + " enumValues(includeDeprecated: true) {\n" + + " name\n" + + " description\n" + + " isDeprecated\n" + + " deprecationReason\n" + + " }\n" + + " possibleTypes {\n" + + " ...TypeRef\n" + + " }\n" + + " }\n" + + "\n" + + " fragment InputValue on __InputValue {\n" + + " name\n" + + " description\n" + + " type { ...TypeRef }\n" + + " defaultValue\n" + + " isDeprecated\n" + + " deprecationReason\n" + + " }\n" + + "\n" + + // + // The depth of the types is actually an arbitrary decision. It could be any depth in fact. This depth + // was taken from GraphIQL https://github.com/graphql/graphiql/blob/master/src/utility/introspectionQueries.js + // which uses 7 levels and hence could represent a type like say [[[[[Float!]]]]] + // + "fragment TypeRef on __Type {\n" + + " kind\n" + + " name\n" + + " ofType {\n" + + " kind\n" + + " name\n" + + " ofType {\n" + + " kind\n" + + " name\n" + + " ofType {\n" + + " kind\n" + + " name\n" + + " ofType {\n" + + " kind\n" + + " name\n" + + " ofType {\n" + + " kind\n" + + " name\n" + + " ofType {\n" + + " kind\n" + + " name\n" + + " ofType {\n" + + " kind\n" + + " name\n" + + " }\n" + + " }\n" + + " }\n" + + " }\n" + + " }\n" + + " }\n" + + " }\n" + + " }\n" + + "\n" + + def newIntrospectionQuery = IntrospectionQuery.INTROSPECTION_QUERY; + + then: + oldIntrospectionQuery.replaceAll("\\s+","").equals( + newIntrospectionQuery.replaceAll("\\s+","") + ) + } + + def "test parameterized introspection queries"() { + def spec = ''' + scalar UUID @specifiedBy(url: "https://tools.ietf.org/html/rfc4122") + + directive @repeatableDirective(arg: String) repeatable on FIELD + + """schema description""" + schema { + query: Query + } + + directive @someDirective( + deprecatedArg : String @deprecated + notDeprecatedArg : String + ) repeatable on FIELD + + type Query { + """notDeprecated root field description""" + notDeprecated(arg : InputType @deprecated, notDeprecatedArg : InputType) : Enum + tenDimensionalList : [[[[[[[[[[String]]]]]]]]]] + } + enum Enum { + RED @deprecated + BLUE + } + input InputType { + inputField : String @deprecated + } + ''' + + def graphQL = TestUtil.graphQL(spec).build() + + def parseExecutionResult = { + [ + it.data["__schema"]["types"].find{it["name"] == "Query"}["fields"].find{it["name"] == "notDeprecated"}["description"] != null, // descriptions is true + it.data["__schema"]["types"].find{it["name"] == "UUID"}["specifiedByURL"] != null, // specifiedByUrl is true + it.data["__schema"]["directives"].find{it["name"] == "repeatableDirective"}["isRepeatable"] != null, // directiveIsRepeatable is true + it.data["__schema"]["description"] != null, // schemaDescription is true + it.data["__schema"]["types"].find { it['name'] == 'InputType' }["inputFields"].find({ it["name"] == "inputField" }) != null // inputValueDeprecation is true + ] + } + + when: + def allFalseExecutionResult = graphQL.execute( + IntrospectionQueryBuilder.build( + IntrospectionQueryBuilder.Options.defaultOptions() + .descriptions(false) + .specifiedByUrl(false) + .directiveIsRepeatable(false) + .schemaDescription(false) + .inputValueDeprecation(false) + .typeRefFragmentDepth(5) + ) + ) + then: + !parseExecutionResult(allFalseExecutionResult).any() + allFalseExecutionResult.data["__schema"]["types"].find{it["name"] == "Query"}["fields"].find{it["name"] == "tenDimensionalList"}["type"]["ofType"]["ofType"]["ofType"]["ofType"]["ofType"]["ofType"] == null // typeRefFragmentDepth is 5 + + when: + def allTrueExecutionResult = graphQL.execute( + IntrospectionQueryBuilder.build( + IntrospectionQueryBuilder.Options.defaultOptions() + .descriptions(true) + .specifiedByUrl(true) + .directiveIsRepeatable(true) + .schemaDescription(true) + .inputValueDeprecation(true) + .typeRefFragmentDepth(7) + ) + ) + then: + parseExecutionResult(allTrueExecutionResult).every() + allTrueExecutionResult.data["__schema"]["types"].find{it["name"] == "Query"}["fields"].find{it["name"] == "tenDimensionalList"}["type"]["ofType"]["ofType"]["ofType"]["ofType"]["ofType"]["ofType"]["ofType"]["ofType"] == null // typeRefFragmentDepth is 7 + } } diff --git a/src/test/groovy/graphql/language/PrettyAstPrinterTest.groovy b/src/test/groovy/graphql/language/PrettyAstPrinterTest.groovy new file mode 100644 index 0000000000..0fab95cbf6 --- /dev/null +++ b/src/test/groovy/graphql/language/PrettyAstPrinterTest.groovy @@ -0,0 +1,521 @@ +package graphql.language + +import spock.lang.Specification + +class PrettyAstPrinterTest extends Specification { + + def "can print type with comments"() { + given: + def input = ''' +# before description +""" Description """ +# before def #1 +# before def #2 +type Query { # beginning of block +a: A b: B + # end of block inside #1 +# end of block inside #2 +} # end of block +''' + + def expected = '''# before description +""" + Description +""" +# before def #1 +# before def #2 +type Query { # beginning of block + a: A + b: B + # end of block inside #1 + # end of block inside #2 +} # end of block +''' + when: + def result = print(input) + + then: + result == expected + } + + + def "can print type with no comments"() { + given: + def input = ''' +""" Description """ +type Query { +a: A b: B} +''' + + def expected = '''""" + Description +""" +type Query { + a: A + b: B +} +''' + when: + def result = print(input) + then: + result == expected + } + + + def "can print interface with comments"() { + given: + def input = ''' +# before description +""" Description """ +# before def #1 +# before def #2 +interface MyInterface { # beginning of block +a: A b: B} # end of block +''' + + def expected = '''# before description +""" + Description +""" +# before def #1 +# before def #2 +interface MyInterface { # beginning of block + a: A + b: B +} # end of block +''' + when: + def result = print(input) + + then: + result == expected + } + + + def "can print interface with no comments"() { + given: + def input = ''' +""" Description """ +interface MyInterface { +a: A b: B} +''' + + def expected = '''""" + Description +""" +interface MyInterface { + a: A + b: B +} +''' + when: + def result = print(input) + then: + result == expected + } + + + def "can print fields with comments"() { + given: + def input = ''' +# before type +type MyType { # beginning of block + +# before field A #1 + +# before field A #2 +""" Description fieldA """ +# before field A #3 + a(arg1: String, arg2: String, arg3: String): A # after field A + # before field B #1 +b(arg1: String, arg2: String, arg3: String): B # after field B +} # end of block +''' + + def expected = '''# before type +type MyType { # beginning of block + # before field A #1 + # before field A #2 + """ + Description fieldA + """ + # before field A #3 + a(arg1: String, arg2: String, arg3: String): A # after field A + # before field B #1 + b(arg1: String, arg2: String, arg3: String): B # after field B +} # end of block +''' + when: + def result = print(input) + + then: + result == expected + } + + + def "can print field arguments with comments"() { + given: + def input = '''type MyType { +""" Description fieldA """ + a(arg1: String # arg1 #1 + # arg2 #1 + """ Description arg2 """ + # arg2 #2 + arg2: String, arg3: String # arg3 #1 + # after all args + ): A # after field A +} +''' + + def expected = '''type MyType { + """ + Description fieldA + """ + a( + arg1: String # arg1 #1 + # arg2 #1 + """ + Description arg2 + """ + # arg2 #2 + arg2: String + arg3: String # arg3 #1 + # after all args + ): A # after field A +} +''' + when: + def result = print(input) + + then: + result == expected + } + + + def "can print schema keeping document level comments"() { + given: + def input = ''' # start of document + +# before Query def + +type Query {a: A b: B} +type MyType {field(id: ID!): MyType! listField: [MyType!]!} +enum MyEnum {VALUE_1 VALUE_2} +# end of document #1 +# end of document #2 +''' + + def expected = """# start of document +# before Query def +type Query { + a: A + b: B +} + +type MyType { + field(id: ID!): MyType! + listField: [MyType!]! +} + +enum MyEnum { + VALUE_1 + VALUE_2 +} +# end of document #1 +# end of document #2 +""" + + when: + def result = print(input) + then: + + result == expected + } + + + def "can print comments between implements"() { + given: + def input = ''' +type MyType implements A & +# interface B #1 + # interface B #2 + B + & + # interface C + C {a: A} +''' + + def expected = '''type MyType implements + A & + # interface B #1 + # interface B #2 + B & + # interface C + C + { + a: A +} +''' + + when: + def result = print(input) + then: + + result == expected + } + + + def "can print type implementing interfaces with no comments"() { + given: + def input = ''' +type MyType implements A & + B & + C + & D + & E { # trailing comment + a: A} +''' + + def expected = '''type MyType implements A & B & C & D & E { # trailing comment + a: A +} +''' + + when: + def result = print(input) + then: + + result == expected + } + + + def "can print enums without comments"() { + given: + def input = ''' +enum MyEnum { + VALUE_1 +VALUE_2 VALUE_3 +} +''' + + def expected = '''enum MyEnum { + VALUE_1 + VALUE_2 + VALUE_3 +} +''' + + when: + def result = print(input) + then: + + result == expected + } + + + def "can print comments in enums"() { + given: + def input = ''' +# before def +enum MyEnum { # inside block #1 +# before VALUE_1 #1 + """ VALUE_1 description """ + # before VALUE_1 #2 + VALUE_1 # after VALUE_1 +VALUE_2 + #inside block #2 +} +''' + + def expected = '''# before def +enum MyEnum { # inside block #1 + # before VALUE_1 #1 + """ + VALUE_1 description + """ + # before VALUE_1 #2 + VALUE_1 # after VALUE_1 + VALUE_2 + #inside block #2 +} +''' + + when: + def result = print(input) + then: + + result == expected + } + + + def "can print comments in scalars"() { + given: + def input = ''' +# before def #1 +""" Description """ + # before def #2 +scalar +MyScalar # def trailing +# after def +''' + + def expected = '''# before def #1 +""" + Description +""" +# before def #2 +scalar MyScalar # def trailing +# after def +''' + + when: + def result = print(input) + then: + + result == expected + } + + + def "can print comments in directives"() { + given: + def input = ''' +# before def #1 +""" Description def """ + # before def #2 +directive +@myDirective( # inside block #1 + # arg1 #1 + """ Description arg1 """ + + # arg1 #2 + arg1: String! # arg1 trailing + # arg2 #1 + arg2: ID arg3: String! # arg3 trailing + # inside block #1 + ) on FIELD_DEFINITION # trailing def +# after def +''' + + def expected = '''# before def #1 +""" + Description def +""" +# before def #2 +directive @myDirective( # inside block #1 + # arg1 #1 + """ + Description arg1 + """ + # arg1 #2 + arg1: String! # arg1 trailing + # arg2 #1 + arg2: ID + arg3: String! # arg3 trailing + # inside block #1 +) on FIELD_DEFINITION # trailing def +# after def +''' + + when: + def result = print(input) + then: + + result == expected + } + + def "can print extended type with comments"() { + given: + def input = ''' +# before description +extend type Query { # beginning of block +a: A b: B + # end of block inside #1 +# end of block inside #2 +} # end of block +''' + + def expected = '''# before description +extend type Query { # beginning of block + a: A + b: B + # end of block inside #1 + # end of block inside #2 +} # end of block +''' + when: + def result = print(input) + + then: + result == expected + } + + + def "can print input type with comments"() { + given: + def input = ''' +# before description +""" Description """ +# before def #1 +# before def #2 +input MyInput { # beginning of block +a: A b: B + # end of block inside #1 +# end of block inside #2 +} # end of block +''' + + def expected = '''# before description +""" + Description +""" +# before def #1 +# before def #2 +input MyInput { # beginning of block + a: A + b: B + # end of block inside #1 + # end of block inside #2 +} # end of block +''' + when: + def result = print(input) + + then: + result == expected + } + + def "can use print with tab indent"() { + given: + def input = ''' +type Query { +field( +# comment +a: A, b: B): Type +} +''' + + def expected = '''type Query { +\tfield( +\t\t# comment +\t\ta: A +\t\tb: B +\t): Type +} +''' + when: + def options = PrettyAstPrinter.PrettyPrinterOptions + .builder() + .indentType(PrettyAstPrinter.PrettyPrinterOptions.IndentType.TAB) + .indentWith(1) + .build() + + def result = PrettyAstPrinter.print(input, options) + + then: + result == expected + } + + private static String print(String input) { + return PrettyAstPrinter.print(input, PrettyAstPrinter.PrettyPrinterOptions.defaultOptions()) + } +} diff --git a/src/test/groovy/graphql/nextgen/GraphqlNextGenTest.groovy b/src/test/groovy/graphql/nextgen/GraphqlNextGenTest.groovy deleted file mode 100644 index 5b59e6ca9c..0000000000 --- a/src/test/groovy/graphql/nextgen/GraphqlNextGenTest.groovy +++ /dev/null @@ -1,32 +0,0 @@ -package graphql.nextgen - - -import graphql.schema.DataFetcher -import spock.lang.Specification - -import static graphql.ExecutionInput.newExecutionInput -import static graphql.TestUtil.schema - -class GraphqlNextGenTest extends Specification { - - def "simple query"() { - given: - def dataFetchers = [ - Query: [hello: { env -> "world" } as DataFetcher] - ] - - def schema = schema(''' - type Query { - hello : String! - } - ''', dataFetchers) - - def graphQL = GraphQL.newGraphQL(schema).build() - - when: - def result = graphQL.executeAsync(newExecutionInput('{ hello }')).get() - - then: - result.data == [hello: 'world'] - } -} diff --git a/src/test/groovy/graphql/normalized/ExecutableNormalizedFieldTest.groovy b/src/test/groovy/graphql/normalized/ExecutableNormalizedFieldTest.groovy new file mode 100644 index 0000000000..dc3db5daa4 --- /dev/null +++ b/src/test/groovy/graphql/normalized/ExecutableNormalizedFieldTest.groovy @@ -0,0 +1,63 @@ +package graphql.normalized + +import graphql.TestUtil +import graphql.execution.CoercedVariables +import graphql.language.Document +import graphql.schema.GraphQLSchema +import spock.lang.Specification + +class ExecutableNormalizedFieldTest extends Specification { + + def "can get children of object type"() { + + String schema = """ + type Query{ + pets: [Pet] + } + interface Pet { + id: ID + name : String! + } + type Cat implements Pet{ + id: ID + name : String! + meow : String + } + type Dog implements Pet{ + id: ID + name : String! + woof : String + } + """ + GraphQLSchema graphQLSchema = TestUtil.schema(schema) + + String query = """ + { + pets { + id + name + ... on Dog { + woof + } + ... on Cat { + meow + } + } + } + + """ + Document document = TestUtil.parseQuery(query) + + ExecutableNormalizedOperationFactory normalizedOperationFactory = new ExecutableNormalizedOperationFactory() + def normalizedOperation = normalizedOperationFactory.createExecutableNormalizedOperation(graphQLSchema, document, null, CoercedVariables.emptyVariables()) + + def pets = normalizedOperation.getTopLevelFields()[0] + def allChildren = pets.getChildren() + def dogFields = pets.getChildren("Dog") + + expect: + allChildren.collect { it.name } == ["id", "name", "woof", "meow"] + dogFields.collect { it.name } == ["id", "name", "woof"] + } + +} diff --git a/src/test/groovy/graphql/normalized/ExecutableNormalizedOperationFactoryTest.groovy b/src/test/groovy/graphql/normalized/ExecutableNormalizedOperationFactoryTest.groovy index 1b18fa97f3..9472680f47 100644 --- a/src/test/groovy/graphql/normalized/ExecutableNormalizedOperationFactoryTest.groovy +++ b/src/test/groovy/graphql/normalized/ExecutableNormalizedOperationFactoryTest.groovy @@ -1,5 +1,6 @@ package graphql.normalized +import graphql.ExecutionInput import graphql.GraphQL import graphql.TestUtil import graphql.execution.CoercedVariables @@ -1314,7 +1315,8 @@ schema { private void assertValidQuery(GraphQLSchema graphQLSchema, String query, Map variables = [:]) { GraphQL graphQL = GraphQL.newGraphQL(graphQLSchema).build() - assert graphQL.execute(query, null, variables).errors.size() == 0 + def ei = ExecutionInput.newExecutionInput(query).variables(variables).build() + assert graphQL.execute(ei).errors.size() == 0 } def "normalized arguments"() { @@ -1393,9 +1395,8 @@ schema { assertValidQuery(graphQLSchema, query) def document = TestUtil.parseQuery(query) def dependencyGraph = new ExecutableNormalizedOperationFactory() - def variables = [:] when: - def tree = dependencyGraph.createExecutableNormalizedOperationWithRawVariables(graphQLSchema, document, null, RawVariables.of(variables)) + def tree = dependencyGraph.createExecutableNormalizedOperationWithRawVariables(graphQLSchema, document, null, RawVariables.emptyVariables()) then: def topLevelField = tree.getTopLevelFields().get(0) diff --git a/src/test/groovy/graphql/parser/ParserTest.groovy b/src/test/groovy/graphql/parser/ParserTest.groovy index 6a76ace1b4..6dcf336742 100644 --- a/src/test/groovy/graphql/parser/ParserTest.groovy +++ b/src/test/groovy/graphql/parser/ParserTest.groovy @@ -40,6 +40,7 @@ import graphql.language.TypeName import graphql.language.UnionTypeDefinition import graphql.language.VariableDefinition import graphql.language.VariableReference +import graphql.parser.exceptions.ParseCancelledException import org.antlr.v4.runtime.CommonTokenStream import org.antlr.v4.runtime.ParserRuleContext import spock.lang.Issue @@ -605,7 +606,7 @@ class ParserTest extends Specification { then: def e = thrown(InvalidSyntaxException) - e.message.contains("Invalid Syntax") + e.message.contains("Invalid syntax") } def "three quotation marks is an illegal string"() { @@ -617,7 +618,7 @@ class ParserTest extends Specification { then: def e = thrown(InvalidSyntaxException) - e.message.contains("Invalid Syntax") + e.message.contains("Invalid syntax") } def "escaped triple quote inside block string"() { @@ -836,7 +837,7 @@ triple3 : """edge cases \\""" "" " \\"" \\" edge cases""" println document then: def e = thrown(InvalidSyntaxException) - e.message.contains("Invalid Syntax") + e.message.contains("Invalid syntax") e.sourcePreview == input + "\n" e.location.line == 3 e.location.column == 20 @@ -858,6 +859,24 @@ triple3 : """edge cases \\""" "" " \\"" \\" edge cases""" operationDefinition.getComments()[0].content == " Represents the 😕 emoji." } + + def "the parser can be invoked via parser environment"() { + def input = ''' + # Represents the 😕 emoji. + { + foo + } + ''' + when: + def parserEnvironment = ParserEnvironment.newParserEnvironment().document(input).build() + + Document document = Parser.parse(parserEnvironment) + OperationDefinition operationDefinition = (document.definitions[0] as OperationDefinition) + + then: + operationDefinition.getComments()[0].content == " Represents the 😕 emoji." + } + def "can override antlr to ast"() { def query = ''' @@ -868,9 +887,9 @@ triple3 : """edge cases \\""" "" " \\"" \\" edge cases""" when: Parser parser = new Parser() { @Override - protected GraphqlAntlrToLanguage getAntlrToLanguage(CommonTokenStream tokens, MultiSourceReader multiSourceReader) { + protected GraphqlAntlrToLanguage getAntlrToLanguage(CommonTokenStream tokens, MultiSourceReader multiSourceReader, ParserEnvironment environment) { // this pattern is used in Nadel - its backdoor but needed - return new GraphqlAntlrToLanguage(tokens, multiSourceReader) { + return new GraphqlAntlrToLanguage(tokens, multiSourceReader, environment.parserOptions, environment.i18N, null) { @Override protected void addCommonData(NodeBuilder nodeBuilder, ParserRuleContext parserRuleContext) { super.addCommonData(nodeBuilder, parserRuleContext) @@ -890,8 +909,8 @@ triple3 : """edge cases \\""" "" " \\"" \\" edge cases""" parser = new Parser() { @Override - protected GraphqlAntlrToLanguage getAntlrToLanguage(CommonTokenStream tokens, MultiSourceReader multiSourceReader, ParserOptions parserOptions) { - return new GraphqlAntlrToLanguage(tokens, multiSourceReader, parserOptions) { + protected GraphqlAntlrToLanguage getAntlrToLanguage(CommonTokenStream tokens, MultiSourceReader multiSourceReader, ParserEnvironment environment) { + return new GraphqlAntlrToLanguage(tokens, multiSourceReader, environment.parserOptions, environment.i18N, null) { @Override protected void addCommonData(NodeBuilder nodeBuilder, ParserRuleContext parserRuleContext) { super.addCommonData(nodeBuilder, parserRuleContext) @@ -933,7 +952,7 @@ triple3 : """edge cases \\""" "" " \\"" \\" edge cases""" then: def e = thrown(InvalidSyntaxException) - e.message.contains("Invalid Syntax") + e.message.contains("Invalid syntax") where: value | _ '00' | _ @@ -953,7 +972,7 @@ triple3 : """edge cases \\""" "" " \\"" \\" edge cases""" then: def e = thrown(InvalidSyntaxException) - e.message.contains("Invalid Syntax") + e.message.contains("Invalid syntax") where: value | _ '01.23' | _ @@ -1108,7 +1127,7 @@ triple3 : """edge cases \\""" "" " \\"" \\" edge cases""" then: InvalidSyntaxException e = thrown(InvalidSyntaxException) - e.message == "Invalid Syntax : Invalid unicode - leading surrogate must be followed by a trailing surrogate - offending token '\\ud83c' at line 3 column 24" + e.message == "Invalid unicode encountered. Leading surrogate must be followed by a trailing surrogate. Offending token '\\ud83c' at line 3 column 24" } def "invalid surrogate pair - no leading value"() { @@ -1124,7 +1143,7 @@ triple3 : """edge cases \\""" "" " \\"" \\" edge cases""" then: InvalidSyntaxException e = thrown(InvalidSyntaxException) - e.message == "Invalid Syntax : Invalid unicode - trailing surrogate must be preceded with a leading surrogate - offending token '\\uDC00' at line 3 column 24" + e.message == "Invalid unicode encountered. Trailing surrogate must be preceded with a leading surrogate. Offending token '\\uDC00' at line 3 column 24" } def "source locations are on by default but can be turned off"() { diff --git a/src/test/groovy/graphql/parser/StringValueParsingTest.groovy b/src/test/groovy/graphql/parser/StringValueParsingTest.groovy index 5543dbc339..2142c84175 100644 --- a/src/test/groovy/graphql/parser/StringValueParsingTest.groovy +++ b/src/test/groovy/graphql/parser/StringValueParsingTest.groovy @@ -1,5 +1,7 @@ package graphql.parser +import graphql.i18n.I18n +import graphql.language.SourceLocation import spock.lang.Specification import static java.util.Arrays.asList @@ -7,12 +9,15 @@ import static java.util.stream.Collectors.joining class StringValueParsingTest extends Specification { + def i18n = I18n.i18n(I18n.BundleType.Parsing, Locale.ENGLISH) + def sourceLocation = SourceLocation.EMPTY + def "parsing quoted string should work"() { given: def input = '''"simple quoted"''' when: - String parsed = StringValueParsing.parseSingleQuotedString(input) + String parsed = StringValueParsing.parseSingleQuotedString(i18n, input,sourceLocation) then: parsed == "simple quoted" @@ -23,7 +28,7 @@ class StringValueParsingTest extends Specification { def input = '''"{\"name\": \"graphql\", \"year\": 2015}"''' when: - String parsed = StringValueParsing.parseSingleQuotedString(input) + String parsed = StringValueParsing.parseSingleQuotedString(i18n, input,sourceLocation) then: parsed == '''{\"name\": \"graphql\", \"year\": 2015}''' @@ -34,7 +39,7 @@ class StringValueParsingTest extends Specification { def input = '''"""''' when: - String parsed = StringValueParsing.parseSingleQuotedString(input) + String parsed = StringValueParsing.parseSingleQuotedString(i18n, input,sourceLocation) then: parsed == '''"''' @@ -45,7 +50,7 @@ class StringValueParsingTest extends Specification { def input = '''"\\ud83c\\udf7a"''' when: - String parsed = StringValueParsing.parseSingleQuotedString(input) + String parsed = StringValueParsing.parseSingleQuotedString(i18n, input,sourceLocation) then: parsed == '''🍺''' // contains the beer icon U+1F37A : http://www.charbase.com/1f37a-unicode-beer-mug @@ -56,7 +61,7 @@ class StringValueParsingTest extends Specification { def input = '''"\\u5564\\u9152"''' when: - String parsed = StringValueParsing.parseSingleQuotedString(input) + String parsed = StringValueParsing.parseSingleQuotedString(i18n, input,sourceLocation) then: parsed == '''啤酒''' diff --git a/src/test/groovy/graphql/parser/StringValueParsingUnicodeTest.groovy b/src/test/groovy/graphql/parser/StringValueParsingUnicodeTest.groovy index 63c59d8011..192f3b0097 100644 --- a/src/test/groovy/graphql/parser/StringValueParsingUnicodeTest.groovy +++ b/src/test/groovy/graphql/parser/StringValueParsingUnicodeTest.groovy @@ -1,12 +1,14 @@ package graphql.parser -import graphql.language.Document -import graphql.language.Field -import graphql.language.OperationDefinition -import graphql.language.StringValue +import graphql.i18n.I18n +import graphql.language.SourceLocation import spock.lang.Specification class StringValueParsingUnicodeTest extends Specification { + + def i18n = I18n.i18n(I18n.BundleType.Parsing, Locale.ENGLISH) + def sourceLocation = SourceLocation.EMPTY + /** * Implements RFC to support full Unicode https://github.com/graphql/graphql-spec/pull/849 * @@ -25,7 +27,7 @@ class StringValueParsingUnicodeTest extends Specification { def input = '''"\\u{1F37A} hello"''' when: - String parsed = StringValueParsing.parseSingleQuotedString(input) + String parsed = StringValueParsing.parseSingleQuotedString(i18n, input,sourceLocation) then: parsed == '''🍺 hello''' // contains the beer icon U+1F37A : http://www.charbase.com/1f37a-unicode-beer-mug @@ -36,7 +38,7 @@ class StringValueParsingUnicodeTest extends Specification { def input = '''"🍺 hello"''' when: - String parsed = StringValueParsing.parseSingleQuotedString(input) + String parsed = StringValueParsing.parseSingleQuotedString(i18n, input,sourceLocation) then: parsed == '''🍺 hello''' // contains the beer icon U+1F37A : http://www.charbase.com/1f37a-unicode-beer-mug @@ -60,11 +62,11 @@ class StringValueParsingUnicodeTest extends Specification { def input = '''"\\uD83D hello"''' when: - StringValueParsing.parseSingleQuotedString(input) + StringValueParsing.parseSingleQuotedString(i18n, input,sourceLocation) then: InvalidSyntaxException e = thrown(InvalidSyntaxException) - e.message == "Invalid Syntax : Invalid unicode - leading surrogate must be followed by a trailing surrogate - offending token '\\uD83D'" + e.message == "Invalid unicode encountered. Leading surrogate must be followed by a trailing surrogate. Offending token '\\uD83D' at line -1 column -1" } def "invalid surrogate pair - end of string"() { @@ -72,11 +74,11 @@ class StringValueParsingUnicodeTest extends Specification { def input = '''"\\uD83D"''' when: - StringValueParsing.parseSingleQuotedString(input) + StringValueParsing.parseSingleQuotedString(i18n, input,sourceLocation) then: InvalidSyntaxException e = thrown(InvalidSyntaxException) - e.message == "Invalid Syntax : Invalid unicode - leading surrogate must be followed by a trailing surrogate - offending token '\\uD83D'" + e.message == "Invalid unicode encountered. Leading surrogate must be followed by a trailing surrogate. Offending token '\\uD83D' at line -1 column -1" } def "invalid surrogate pair - invalid trailing value"() { @@ -84,11 +86,11 @@ class StringValueParsingUnicodeTest extends Specification { def input = '''"\\uD83D\\uDBFF"''' when: - StringValueParsing.parseSingleQuotedString(input) + StringValueParsing.parseSingleQuotedString(i18n, input,sourceLocation) then: InvalidSyntaxException e = thrown(InvalidSyntaxException) - e.message == "Invalid Syntax : Invalid unicode - leading surrogate must be followed by a trailing surrogate - offending token '\\uDBFF'" + e.message == "Invalid unicode encountered. Leading surrogate must be followed by a trailing surrogate. Offending token '\\uDBFF' at line -1 column -1" } def "invalid surrogate pair - no leading value"() { @@ -96,11 +98,11 @@ class StringValueParsingUnicodeTest extends Specification { def input = '''"\\uDC00"''' when: - StringValueParsing.parseSingleQuotedString(input) + StringValueParsing.parseSingleQuotedString(i18n, input,sourceLocation) then: InvalidSyntaxException e = thrown(InvalidSyntaxException) - e.message == "Invalid Syntax : Invalid unicode - trailing surrogate must be preceded with a leading surrogate - offending token '\\uDC00'" + e.message == "Invalid unicode encountered. Trailing surrogate must be preceded with a leading surrogate. Offending token '\\uDC00' at line -1 column -1" } def "invalid surrogate pair - invalid leading value"() { @@ -108,11 +110,11 @@ class StringValueParsingUnicodeTest extends Specification { def input = '''"\\uD700\\uDC00"''' when: - StringValueParsing.parseSingleQuotedString(input) + StringValueParsing.parseSingleQuotedString(i18n, input,sourceLocation) then: InvalidSyntaxException e = thrown(InvalidSyntaxException) - e.message == "Invalid Syntax : Invalid unicode - trailing surrogate must be preceded with a leading surrogate - offending token '\\uDC00'" + e.message == "Invalid unicode encountered. Trailing surrogate must be preceded with a leading surrogate. Offending token '\\uDC00' at line -1 column -1" } def "valid surrogate pair - leading code with braces"() { @@ -120,7 +122,7 @@ class StringValueParsingUnicodeTest extends Specification { def input = '''"hello \\u{d83c}\\udf7a"''' when: - String parsed = StringValueParsing.parseSingleQuotedString(input) + String parsed = StringValueParsing.parseSingleQuotedString(i18n, input,sourceLocation) then: parsed == '''hello 🍺''' // contains the beer icon U+1F37 A : http://www.charbase.com/1f37a-unicode-beer-mug @@ -131,7 +133,7 @@ class StringValueParsingUnicodeTest extends Specification { def input = '''"hello \\ud83c\\u{df7a}"''' when: - String parsed = StringValueParsing.parseSingleQuotedString(input) + String parsed = StringValueParsing.parseSingleQuotedString(i18n, input,sourceLocation) then: parsed == '''hello 🍺''' // contains the beer icon U+1F37A : http://www.charbase.com/1f37a-unicode-beer-mug @@ -142,7 +144,7 @@ class StringValueParsingUnicodeTest extends Specification { def input = '''"hello \\u{d83c}\\u{df7a}"''' when: - String parsed = StringValueParsing.parseSingleQuotedString(input) + String parsed = StringValueParsing.parseSingleQuotedString(i18n, input,sourceLocation) then: parsed == '''hello 🍺''' // contains the beer icon U+1F37A : http://www.charbase.com/1f37a-unicode-beer-mug @@ -153,11 +155,11 @@ class StringValueParsingUnicodeTest extends Specification { def input = '''"hello \\u{d83c}\\"''' when: - StringValueParsing.parseSingleQuotedString(input) + StringValueParsing.parseSingleQuotedString(i18n, input,sourceLocation) then: InvalidSyntaxException e = thrown(InvalidSyntaxException) - e.message == "Invalid Syntax : Invalid unicode - leading surrogate must be followed by a trailing surrogate - offending token '\\u{d83c}'" + e.message == "Invalid unicode encountered. Leading surrogate must be followed by a trailing surrogate. Offending token '\\u{d83c}' at line -1 column -1" } def "invalid surrogate pair - leading code with only \\u at end of string"() { @@ -165,11 +167,11 @@ class StringValueParsingUnicodeTest extends Specification { def input = '''"hello \\u{d83c}\\u"''' when: - StringValueParsing.parseSingleQuotedString(input) + StringValueParsing.parseSingleQuotedString(i18n, input,sourceLocation) then: InvalidSyntaxException e = thrown(InvalidSyntaxException) - e.message == "Invalid Syntax : Invalid unicode - incorrectly formatted escape - offending token '\\u\"'" + e.message == "Invalid unicode encountered. Incorrectly formatted escape sequence. Offending token '\\u\"' at line -1 column -1" } def "invalid surrogate pair - trailing code without closing brace"() { @@ -177,11 +179,11 @@ class StringValueParsingUnicodeTest extends Specification { def input = '''"hello \\u{d83c}\\u{df7a"''' when: - StringValueParsing.parseSingleQuotedString(input) + StringValueParsing.parseSingleQuotedString(i18n, input,sourceLocation) then: InvalidSyntaxException e = thrown(InvalidSyntaxException) - e.message == "Invalid Syntax : Invalid unicode - incorrectly formatted escape - offending token '\\u{df7a'" + e.message == "Invalid unicode encountered. Incorrectly formatted escape sequence. Offending token '\\u{df7a' at line -1 column -1" } def "invalid surrogate pair - invalid trailing code without unicode escape 1"() { @@ -189,11 +191,11 @@ class StringValueParsingUnicodeTest extends Specification { def input = '''"hello \\u{d83c}{df7a}"''' when: - StringValueParsing.parseSingleQuotedString(input) + StringValueParsing.parseSingleQuotedString(i18n, input,sourceLocation) then: InvalidSyntaxException e = thrown(InvalidSyntaxException) - e.message == "Invalid Syntax : Invalid unicode - leading surrogate must be followed by a trailing surrogate - offending token '\\u{d83c}'" + e.message == "Invalid unicode encountered. Leading surrogate must be followed by a trailing surrogate. Offending token '\\u{d83c}' at line -1 column -1" } def "invalid surrogate pair - invalid trailing code without unicode escape 2"() { @@ -201,11 +203,11 @@ class StringValueParsingUnicodeTest extends Specification { def input = '''"hello \\u{d83c}df7a"''' when: - StringValueParsing.parseSingleQuotedString(input) + StringValueParsing.parseSingleQuotedString(i18n, input,sourceLocation) then: InvalidSyntaxException e = thrown(InvalidSyntaxException) - e.message == "Invalid Syntax : Invalid unicode - leading surrogate must be followed by a trailing surrogate - offending token '\\u{d83c}'" + e.message == "Invalid unicode encountered. Leading surrogate must be followed by a trailing surrogate. Offending token '\\u{d83c}' at line -1 column -1" } def "invalid surrogate pair - invalid leading code"() { @@ -213,11 +215,11 @@ class StringValueParsingUnicodeTest extends Specification { def input = '''"hello d83c\\u{df7a}"''' when: - StringValueParsing.parseSingleQuotedString(input) + StringValueParsing.parseSingleQuotedString(i18n, input,sourceLocation) then: InvalidSyntaxException e = thrown(InvalidSyntaxException) - e.message == "Invalid Syntax : Invalid unicode - trailing surrogate must be preceded with a leading surrogate - offending token '\\u{df7a}'" + e.message == "Invalid unicode encountered. Trailing surrogate must be preceded with a leading surrogate. Offending token '\\u{df7a}' at line -1 column -1" } def "invalid surrogate pair - invalid leading value with braces"() { @@ -225,11 +227,11 @@ class StringValueParsingUnicodeTest extends Specification { def input = '''"\\u{5B57}\\uDC00"''' when: - StringValueParsing.parseSingleQuotedString(input) + StringValueParsing.parseSingleQuotedString(i18n, input,sourceLocation) then: InvalidSyntaxException e = thrown(InvalidSyntaxException) - e.message == "Invalid Syntax : Invalid unicode - trailing surrogate must be preceded with a leading surrogate - offending token '\\uDC00'" + e.message == "Invalid unicode encountered. Trailing surrogate must be preceded with a leading surrogate. Offending token '\\uDC00' at line -1 column -1" } def "invalid surrogate pair - invalid trailing value with braces"() { @@ -237,11 +239,11 @@ class StringValueParsingUnicodeTest extends Specification { def input = '''"\\uD83D\\u{DBFF}"''' when: - StringValueParsing.parseSingleQuotedString(input) + StringValueParsing.parseSingleQuotedString(i18n, input,sourceLocation) then: InvalidSyntaxException e = thrown(InvalidSyntaxException) - e.message == "Invalid Syntax : Invalid unicode - leading surrogate must be followed by a trailing surrogate - offending token '\\u{DBFF}'" + e.message == "Invalid unicode encountered. Leading surrogate must be followed by a trailing surrogate. Offending token '\\u{DBFF}' at line -1 column -1" } def "invalid unicode code point - value is too high"() { @@ -249,10 +251,10 @@ class StringValueParsingUnicodeTest extends Specification { def input = '''"\\u{fffffff}"''' when: - StringValueParsing.parseSingleQuotedString(input) + StringValueParsing.parseSingleQuotedString(i18n, input,sourceLocation) then: InvalidSyntaxException e = thrown(InvalidSyntaxException) - e.message == "Invalid Syntax : Invalid unicode - not a valid code point - offending token '\\u{fffffff}'" + e.message == "Invalid unicode encountered. Not a valid code point. Offending token '\\u{fffffff}' at line -1 column -1" } } diff --git a/src/test/groovy/graphql/relay/DefaultConnectionTest.groovy b/src/test/groovy/graphql/relay/DefaultConnectionTest.groovy new file mode 100644 index 0000000000..592b18c968 --- /dev/null +++ b/src/test/groovy/graphql/relay/DefaultConnectionTest.groovy @@ -0,0 +1,27 @@ +package graphql.relay + +import com.google.common.collect.ImmutableList +import spock.lang.Specification + +class DefaultConnectionTest extends Specification { + + def "test equality and hashcode"() { + def edges1 = ImmutableList.of(new DefaultEdge("a", new DefaultConnectionCursor("a"))) + def edges2 = ImmutableList.of(new DefaultEdge("b", new DefaultConnectionCursor("b"))) + + def pageInfo1 = new DefaultPageInfo(new DefaultConnectionCursor("c"), new DefaultConnectionCursor("c"), true, false) + def pageInfo2 = new DefaultPageInfo(new DefaultConnectionCursor("d"), new DefaultConnectionCursor("d"), false, true) + + expect: + + assert new DefaultConnection(edges1, pageInfo1).equals(new DefaultConnection(edges1, pageInfo1)) + assert !new DefaultConnection(edges1, pageInfo2).equals(new DefaultConnection(edges1, pageInfo1)) + assert !new DefaultConnection(edges1, pageInfo1).equals(new DefaultConnection(edges2, pageInfo1)) + assert !new DefaultConnection(edges1, pageInfo1).equals(new DefaultConnection(edges1, pageInfo2)) + + assert new DefaultConnection(edges1, pageInfo1).hashCode() == new DefaultConnection(edges1, pageInfo1).hashCode() + assert new DefaultConnection(edges1, pageInfo2).hashCode() != new DefaultConnection(edges1, pageInfo1).hashCode() + assert new DefaultConnection(edges1, pageInfo1).hashCode() != new DefaultConnection(edges2, pageInfo1).hashCode() + assert new DefaultConnection(edges1, pageInfo1).hashCode() != new DefaultConnection(edges1, pageInfo2).hashCode() + } +} diff --git a/src/test/groovy/graphql/relay/DefaultEdgeTest.groovy b/src/test/groovy/graphql/relay/DefaultEdgeTest.groovy new file mode 100644 index 0000000000..3763a0c7ab --- /dev/null +++ b/src/test/groovy/graphql/relay/DefaultEdgeTest.groovy @@ -0,0 +1,25 @@ +package graphql.relay + +import spock.lang.Specification + +class DefaultEdgeTest extends Specification { + + def "test equality and hashcode"() { + expect: + + assert new DefaultEdge(leftNode, new DefaultConnectionCursor(leftConnectionCursor)).equals( + new DefaultEdge(rightNode, new DefaultConnectionCursor(rightConnectionCursor))) == isEqual + + assert (new DefaultEdge(leftNode, new DefaultConnectionCursor(leftConnectionCursor)).hashCode() == + new DefaultEdge(rightNode, new DefaultConnectionCursor(rightConnectionCursor)).hashCode()) == isEqual + + where: + + leftNode | leftConnectionCursor | rightNode | rightConnectionCursor || isEqual + "a" | "b" | "a" | "b" || true + "x" | "b" | "a" | "b" || false + "a" | "x" | "a" | "b" || false + "a" | "b" | "x" | "b" || false + "a" | "b" | "a" | "x" || false + } +} diff --git a/src/test/groovy/graphql/relay/DefaultPageInfoTest.groovy b/src/test/groovy/graphql/relay/DefaultPageInfoTest.groovy new file mode 100644 index 0000000000..2e370467cb --- /dev/null +++ b/src/test/groovy/graphql/relay/DefaultPageInfoTest.groovy @@ -0,0 +1,29 @@ +package graphql.relay + +import spock.lang.Specification + +class DefaultPageInfoTest extends Specification { + + def "test equality and hashcode"() { + expect: + + assert new DefaultPageInfo(new DefaultConnectionCursor(ls), new DefaultConnectionCursor(le), lp, ln).equals( + new DefaultPageInfo(new DefaultConnectionCursor(rs), new DefaultConnectionCursor(re), rp, rn)) == isEqual + + assert (new DefaultPageInfo(new DefaultConnectionCursor(ls), new DefaultConnectionCursor(le), lp, ln).hashCode() == + new DefaultPageInfo(new DefaultConnectionCursor(rs), new DefaultConnectionCursor(re), rp, rn).hashCode()) == isEqual + + where: + + ls | le | lp | ln | rs | re | rp | rn || isEqual + "a" | "b" | true | true | "a" | "b" | true | true || true + "x" | "b" | true | true | "a" | "b" | true | true || false + "a" | "x" | true | true | "a" | "b" | true | true || false + "a" | "b" | false | true | "a" | "b" | true | true || false + "a" | "b" | true | false | "a" | "b" | true | true || false + "a" | "b" | true | true | "x" | "b" | true | true || false + "a" | "b" | true | true | "a" | "x" | true | true || false + "a" | "b" | true | true | "a" | "b" | false | true || false + "a" | "b" | true | true | "a" | "b" | true | false || false + } +} diff --git a/src/test/groovy/graphql/schema/DataFetchingEnvironmentImplTest.groovy b/src/test/groovy/graphql/schema/DataFetchingEnvironmentImplTest.groovy index ef348db2c0..2830419999 100644 --- a/src/test/groovy/graphql/schema/DataFetchingEnvironmentImplTest.groovy +++ b/src/test/groovy/graphql/schema/DataFetchingEnvironmentImplTest.groovy @@ -1,20 +1,24 @@ package graphql.schema import graphql.GraphQLContext -import graphql.cachecontrol.CacheControl +import graphql.execution.CoercedVariables import graphql.execution.ExecutionId import graphql.execution.ExecutionStepInfo +import graphql.language.Argument +import graphql.language.Field import graphql.language.FragmentDefinition import graphql.language.OperationDefinition +import graphql.language.StringValue import graphql.language.TypeName import org.dataloader.BatchLoader -import org.dataloader.DataLoader +import org.dataloader.DataLoaderFactory import org.dataloader.DataLoaderRegistry import spock.lang.Specification import java.util.concurrent.CompletableFuture import static graphql.StarWarsSchema.starWarsSchema +import static graphql.TestUtil.mergedField import static graphql.TestUtil.toDocument import static graphql.execution.ExecutionContextBuilder.newExecutionContextBuilder import static graphql.schema.DataFetchingEnvironmentImpl.newDataFetchingEnvironment @@ -23,30 +27,26 @@ class DataFetchingEnvironmentImplTest extends Specification { def frag = FragmentDefinition.newFragmentDefinition().name("frag").typeCondition(new TypeName("t")).build() - def dataLoader = DataLoader.newDataLoader({ keys -> CompletableFuture.completedFuture(keys) } as BatchLoader) + def dataLoader = DataLoaderFactory.newDataLoader({ keys -> CompletableFuture.completedFuture(keys) } as BatchLoader) def operationDefinition = new OperationDefinition("q") def document = toDocument("{ f }") def executionId = ExecutionId.from("123") def fragmentByName = [frag: frag] def variables = [var: "able"] def dataLoaderRegistry = new DataLoaderRegistry().register("dataLoader", dataLoader) - def cacheControl = CacheControl.newCacheControl() def executionContext = newExecutionContextBuilder() .root("root") - .context("context") .graphQLContext(GraphQLContext.of(["key":"context"])) .executionId(executionId) .operationDefinition(operationDefinition) .document(document) - .variables(variables) + .coercedVariables(CoercedVariables.of(variables)) .graphQLSchema(starWarsSchema) .fragmentsByName(fragmentByName) .dataLoaderRegistry(dataLoaderRegistry) - .cacheControl(cacheControl) .build() - def "immutable arguments"() { def dataFetchingEnvironment = newDataFetchingEnvironment(executionContext).arguments([arg: "argVal"]) .build() @@ -62,13 +62,11 @@ class DataFetchingEnvironmentImplTest extends Specification { } def "copying works as expected from execution context"() { - when: def dfe = newDataFetchingEnvironment(executionContext) .build() then: dfe.getRoot() == "root" - dfe.getContext() == "context" dfe.getGraphQlContext().get("key") == "context" dfe.getGraphQLSchema() == starWarsSchema dfe.getDocument() == document @@ -80,7 +78,7 @@ class DataFetchingEnvironmentImplTest extends Specification { def "create environment from existing one will copy everything to new instance"() { def dfe = newDataFetchingEnvironment() - .context("Test Context") + .context("Test Context") // Retain deprecated builder for coverage .graphQLContext(GraphQLContext.of(["key": "context"])) .source("Test Source") .root("Test Root") @@ -96,7 +94,6 @@ class DataFetchingEnvironmentImplTest extends Specification { .document(document) .variables(variables) .dataLoaderRegistry(dataLoaderRegistry) - .cacheControl(cacheControl) .locale(Locale.CANADA) .localContext("localContext") .build() @@ -106,7 +103,7 @@ class DataFetchingEnvironmentImplTest extends Specification { then: dfe != dfeCopy - dfe.getContext() == dfeCopy.getContext() + dfe.getContext() == dfeCopy.getContext() // Retain deprecated method for coverage dfe.getGraphQlContext() == dfeCopy.getGraphQlContext() dfe.getSource() == dfeCopy.getSource() dfe.getRoot() == dfeCopy.getRoot() @@ -122,7 +119,6 @@ class DataFetchingEnvironmentImplTest extends Specification { dfe.getOperationDefinition() == dfeCopy.getOperationDefinition() dfe.getVariables() == dfeCopy.getVariables() dfe.getDataLoader("dataLoader") == dataLoader - dfe.getCacheControl() == cacheControl dfe.getLocale() == dfeCopy.getLocale() dfe.getLocalContext() == dfeCopy.getLocalContext() } @@ -138,4 +134,18 @@ class DataFetchingEnvironmentImplTest extends Specification { dfe.getArgument("x") == "y" dfe.getArgumentOrDefault("x", "default") == "y" } + + def "deprecated getFields() method works"() { + when: + Argument argument = new Argument("arg1", new StringValue("argVal")) + Field field = new Field("someField", [argument]) + + def environment = newDataFetchingEnvironment(executionContext) + .mergedField(mergedField(field)) + .build() + + then: + environment.fields == [field] // Retain deprecated method for test coverage + } + } diff --git a/src/test/groovy/graphql/schema/DelegatingDataFetchingEnvironmentTest.groovy b/src/test/groovy/graphql/schema/DelegatingDataFetchingEnvironmentTest.groovy index 3384a8ab28..687a0f62ae 100644 --- a/src/test/groovy/graphql/schema/DelegatingDataFetchingEnvironmentTest.groovy +++ b/src/test/groovy/graphql/schema/DelegatingDataFetchingEnvironmentTest.groovy @@ -28,7 +28,7 @@ class DelegatingDataFetchingEnvironmentTest extends Specification { } @Override - def getContext() { + def getContext() { // Retain for test coverage return "overriddenContext" } } diff --git a/src/test/groovy/graphql/Issue2001.groovy b/src/test/groovy/graphql/schema/GraphQLAppliedDirectiveArgumentTest.groovy similarity index 69% rename from src/test/groovy/graphql/Issue2001.groovy rename to src/test/groovy/graphql/schema/GraphQLAppliedDirectiveArgumentTest.groovy index 7c39e0132e..35875c78d2 100644 --- a/src/test/groovy/graphql/Issue2001.groovy +++ b/src/test/groovy/graphql/schema/GraphQLAppliedDirectiveArgumentTest.groovy @@ -1,16 +1,16 @@ -package graphql +package graphql.schema +import graphql.GraphQLContext +import graphql.TestUtil import graphql.execution.ValuesResolver -import graphql.schema.GraphQLArgument import graphql.schema.idl.RuntimeWiring import graphql.schema.idl.SchemaGenerator import graphql.schema.idl.SchemaParser import graphql.schema.idl.errors.SchemaProblem import spock.lang.Specification -class Issue2001 extends Specification { - - def "test non-list value for a list argument of a directive"() { +class GraphQLAppliedDirectiveArgumentTest extends Specification { + def "test non-list value for a list argument of a directive - issue 2001"() { def spec = ''' directive @test(value: [String] = "default") on FIELD_DEFINITION type Query { @@ -21,15 +21,15 @@ class Issue2001 extends Specification { ''' def closure = { - def argument = it.fieldDefinition.getDirective("test").getArgument("value") as GraphQLArgument - return ValuesResolver.valueToInternalValue(argument.getArgumentValue(), argument.getType())[0] + def argument = it.fieldDefinition.getAppliedDirective("test").getArgument("value") as GraphQLAppliedDirectiveArgument + return ValuesResolver.valueToInternalValue(argument.getArgumentValue(), argument.getType(), GraphQLContext.getDefault(), Locale.getDefault())[0] } def graphql = TestUtil.graphQL(spec, RuntimeWiring.newRuntimeWiring() - .type("Query", { - it.dataFetcher("testDefaultWorks", closure) - .dataFetcher("testItWorks", closure) - .dataFetcher("testItIsNotBroken", closure) - }).build()) + .type("Query", { + it.dataFetcher("testDefaultWorks", closure) + .dataFetcher("testItWorks", closure) + .dataFetcher("testItIsNotBroken", closure) + }).build()) .build() when: @@ -41,7 +41,8 @@ class Issue2001 extends Specification { result.data.testItWorks == "test" result.data.testItIsNotBroken == "test" } - def "test an incorrect non-list value for a list argument of a directive"() { + + def "test an incorrect non-list value for a list argument of a directive - issue 2001"() { def spec = ''' directive @test(value: [String]) on FIELD_DEFINITION type Query { @@ -49,7 +50,6 @@ class Issue2001 extends Specification { } ''' - when: def reader = new StringReader(spec) def registry = new SchemaParser().parse(reader) diff --git a/src/test/groovy/graphql/schema/GraphQLArgumentTest.groovy b/src/test/groovy/graphql/schema/GraphQLArgumentTest.groovy index 38e3c15388..23a506621d 100644 --- a/src/test/groovy/graphql/schema/GraphQLArgumentTest.groovy +++ b/src/test/groovy/graphql/schema/GraphQLArgumentTest.groovy @@ -1,12 +1,18 @@ package graphql.schema +import graphql.collect.ImmutableKit import graphql.language.FloatValue +import graphql.schema.validation.InvalidSchemaException import spock.lang.Specification import static graphql.Scalars.GraphQLFloat import static graphql.Scalars.GraphQLInt import static graphql.Scalars.GraphQLString +import static graphql.schema.GraphQLArgument.newArgument import static graphql.schema.GraphQLDirective.newDirective +import static graphql.schema.GraphQLFieldDefinition.newFieldDefinition +import static graphql.schema.GraphQLObjectType.newObject +import static graphql.schema.GraphQLSchema.newSchema class GraphQLArgumentTest extends Specification { @@ -25,9 +31,9 @@ class GraphQLArgumentTest extends Specification { .description("A2_description") .type(GraphQLString) .withDirective(newDirective().name("directive3")) - .value("VALUE") + .value("VALUE") // Retain deprecated for test coverage .deprecate(null) - .defaultValue("DEFAULT") + .defaultValue("DEFAULT") // Retain deprecated for test coverage }) then: @@ -43,7 +49,7 @@ class GraphQLArgumentTest extends Specification { transformedArgument.name == "A2" transformedArgument.description == "A2_description" transformedArgument.type == GraphQLString - transformedArgument.argumentValue.value == "VALUE" + transformedArgument.argumentValue.value == "VALUE" // Retain deprecated for test coverage transformedArgument.argumentDefaultValue.value == "DEFAULT" transformedArgument.deprecationReason == null !transformedArgument.isDeprecated() @@ -116,16 +122,17 @@ class GraphQLArgumentTest extends Specification { } def "can get values statically"() { + // Retain deprecated API usages in this test for test coverage when: GraphQLArgument startingArg = GraphQLArgument.newArgument() .name("F1") .type(GraphQLFloat) .description("F1_description") - .valueProgrammatic(4.56d) + .valueProgrammatic(4.56d) // Retain deprecated for test coverage .defaultValueProgrammatic(1.23d) .build() - def inputValue = startingArg.getArgumentValue() - def resolvedValue = GraphQLArgument.getArgumentValue(startingArg) + def inputValue = startingArg.getArgumentValue() // Retain deprecated for test coverage + def resolvedValue = GraphQLArgument.getArgumentValue(startingArg) // Retain deprecated for test coverage def inputDefaultValue = startingArg.getArgumentDefaultValue() def resolvedDefaultValue = GraphQLArgument.getArgumentDefaultValue(startingArg) @@ -144,12 +151,12 @@ class GraphQLArgumentTest extends Specification { .name("F1") .type(GraphQLFloat) .description("F1_description") - .valueLiteral(FloatValue.newFloatValue().value(4.56d).build()) + .valueLiteral(FloatValue.newFloatValue().value(4.56d).build()) // Retain deprecated for test coverage .defaultValueLiteral(FloatValue.newFloatValue().value(1.23d).build()) .build() - inputValue = startingArg.getArgumentValue() - resolvedValue = GraphQLArgument.getArgumentValue(startingArg) + inputValue = startingArg.getArgumentValue() // Retain deprecated for test coverage + resolvedValue = GraphQLArgument.getArgumentValue(startingArg) // Retain deprecated for test coverage inputDefaultValue = startingArg.getArgumentDefaultValue() resolvedDefaultValue = GraphQLArgument.getArgumentDefaultValue(startingArg) @@ -171,8 +178,8 @@ class GraphQLArgumentTest extends Specification { .description("F1_description") .build() - inputValue = startingArg.getArgumentValue() - resolvedValue = GraphQLArgument.getArgumentValue(startingArg) + inputValue = startingArg.getArgumentValue() // Retain deprecated for test coverage + resolvedValue = GraphQLArgument.getArgumentValue(startingArg) // Retain deprecated for test coverage inputDefaultValue = startingArg.getArgumentDefaultValue() resolvedDefaultValue = GraphQLArgument.getArgumentDefaultValue(startingArg) @@ -188,4 +195,26 @@ class GraphQLArgumentTest extends Specification { resolvedDefaultValue == null } + def "Applied schema directives arguments are validated for programmatic schemas"() { + given: + def arg = newArgument().name("arg").type(GraphQLInt).valueProgrammatic(ImmutableKit.emptyMap()).build() // Retain for test coverage + def directive = GraphQLDirective.newDirective().name("cached").argument(arg).build() + def field = newFieldDefinition() + .name("hello") + .type(GraphQLString) + .argument(arg) + .withDirective(directive) + .build() + when: + newSchema().query( + newObject() + .name("Query") + .field(field) + .build()) + .build() + then: + def e = thrown(InvalidSchemaException) + e.message.contains("Invalid argument 'arg' for applied directive of name 'cached'") + } + } diff --git a/src/test/groovy/graphql/schema/GraphQLCodeRegistryTest.groovy b/src/test/groovy/graphql/schema/GraphQLCodeRegistryTest.groovy index b610b6c1a7..eef53c9794 100644 --- a/src/test/groovy/graphql/schema/GraphQLCodeRegistryTest.groovy +++ b/src/test/groovy/graphql/schema/GraphQLCodeRegistryTest.groovy @@ -167,11 +167,15 @@ class GraphQLCodeRegistryTest extends Specification { } def "schema delegates field visibility to code registry"() { - when: - def schema = GraphQLSchema.newSchema().fieldVisibility(new NamedFieldVisibility("B")).query(objectType("query")).build() + def codeRegistry = GraphQLCodeRegistry.newCodeRegistry() + .fieldVisibility(new NamedFieldVisibility("B")) + .build() + def schema = GraphQLSchema.newSchema() + .codeRegistry(codeRegistry) + .query(objectType("query")) + .build() then: - (schema.getFieldVisibility() as NamedFieldVisibility).name == "B" (schema.getCodeRegistry().getFieldVisibility() as NamedFieldVisibility).name == "B" } @@ -226,7 +230,7 @@ class GraphQLCodeRegistryTest extends Specification { .field(newFieldDefinition().name("codeRegistryField").type(Scalars.GraphQLString)) .field(newFieldDefinition().name("nonCodeRegistryField").type(Scalars.GraphQLString) // df comes from the field itself here - .dataFetcher(new NamedDF("nonCodeRegistryFieldValue"))) + .dataFetcher(new NamedDF("nonCodeRegistryFieldValue"))) // Retain to test Field Definition DataFetcher .field(newFieldDefinition().name("neitherSpecified").type(Scalars.GraphQLString)) .build() diff --git a/src/test/groovy/graphql/schema/GraphQLDirectiveTest.groovy b/src/test/groovy/graphql/schema/GraphQLDirectiveTest.groovy index 8d03f982f4..5eba0fbc3a 100644 --- a/src/test/groovy/graphql/schema/GraphQLDirectiveTest.groovy +++ b/src/test/groovy/graphql/schema/GraphQLDirectiveTest.groovy @@ -1,6 +1,5 @@ package graphql.schema -import graphql.AssertException import graphql.TestUtil import graphql.language.Node import spock.lang.Specification @@ -95,13 +94,12 @@ class GraphQLDirectiveTest extends Specification { when: def schema = TestUtil.schema(sdl) then: - schema.getSchemaDirective("d1").name == "d1" - schema.getSchemaDirectiveByName().keySet() == ["d1"] as Set + schema.getSchemaAppliedDirective("d1").name == "d1" + schema.getAllSchemaAppliedDirectivesByName().keySet() == ["d1", "dr"] as Set - schema.getAllSchemaDirectivesByName().keySet() == ["d1", "dr"] as Set - schema.getAllSchemaDirectivesByName()["d1"].size() == 1 - schema.getAllSchemaDirectivesByName()["dr"].size() == 2 - schema.getAllSchemaDirectivesByName()["dr"].collect({ printAst(it.getArgument("arg").argumentValue.value) }) == ['"a1"', '"a2"'] + schema.getAllSchemaAppliedDirectivesByName()["d1"].size() == 1 + schema.getAllSchemaAppliedDirectivesByName()["dr"].size() == 2 + schema.getAllSchemaAppliedDirectivesByName()["dr"].collect({ printAst(it.getArgument("arg").argumentValue.value) }) == ['"a1"', '"a2"'] when: def queryType = schema.getObjectType("Query") @@ -174,20 +172,23 @@ class GraphQLDirectiveTest extends Specification { } static boolean assertDirectiveContainer(GraphQLDirectiveContainer container) { - assert container.hasDirective("d1") - assert container.hasDirective("dr") - assert !container.hasDirective("non existent") - assert container.getDirectives().collect({ it.name }) == ["d1", "dr", "dr"] - assert container.getDirective("d1").name == "d1" - assert container.getDirectivesByName().keySet() == ["d1"] as Set - - assert container.getAllDirectivesByName().keySet() == ["d1", "dr"] as Set - assert container.getAllDirectivesByName()["d1"].size() == 1 - assert container.getAllDirectivesByName()["dr"].size() == 2 - - assert container.getDirectives("d1").size() == 1 - assert container.getDirectives("dr").size() == 2 - assert container.getDirectives("dr").collect({ printAst(it.getArgument("arg").argumentValue.value as Node) }) == ['"a1"', '"a2"'] + assert container.hasDirective("d1") // Retain for test coverage + assert container.hasAppliedDirective("d1") + assert container.hasAppliedDirective("dr") + assert !container.hasAppliedDirective("non existent") + assert container.getDirectives().collect({ it.name }) == ["d1", "dr", "dr"] // Retain for test coverage + assert container.getAppliedDirectives().collect({ it.name }) == ["d1", "dr", "dr"] + assert container.getAppliedDirective("d1").name == "d1" + assert container.getDirectivesByName().keySet() == ["d1"] as Set // Retain for test coverage, there is no equivalent non-repeatable directive method + + assert container.getAllDirectivesByName().keySet() == ["d1", "dr"] as Set // Retain for test coverage + assert container.getAllAppliedDirectivesByName().keySet() == ["d1", "dr"] as Set + assert container.getAllAppliedDirectivesByName()["d1"].size() == 1 + assert container.getAllAppliedDirectivesByName()["dr"].size() == 2 + + assert container.getAppliedDirectives("d1").size() == 1 + assert container.getAppliedDirectives("dr").size() == 2 + assert container.getAppliedDirectives("dr").collect({ printAst(it.getArgument("arg").argumentValue.value as Node) }) == ['"a1"', '"a2"'] return true } diff --git a/src/test/groovy/graphql/schema/GraphQLEnumTypeTest.groovy b/src/test/groovy/graphql/schema/GraphQLEnumTypeTest.groovy index cb832d323f..d717e607a7 100644 --- a/src/test/groovy/graphql/schema/GraphQLEnumTypeTest.groovy +++ b/src/test/groovy/graphql/schema/GraphQLEnumTypeTest.groovy @@ -1,6 +1,7 @@ package graphql.schema import graphql.AssertException +import graphql.GraphQLContext import graphql.language.EnumValue import graphql.language.StringValue import spock.lang.Specification @@ -19,32 +20,52 @@ class GraphQLEnumTypeTest extends Specification { def "parse throws exception for unknown value"() { when: - enumType.parseValue("UNKNOWN") + enumType.parseValue("UNKNOWN", GraphQLContext.default, Locale.default) then: thrown(CoercingParseValueException) } + def "parse throws exception for unknown value with deprecated method"() { + when: + enumType.parseValue("UNKNOWN") // Retain for test coverage + + then: + thrown(CoercingParseValueException) + } def "parse value return value for the name"() { expect: - enumType.parseValue("NAME") == 42 + enumType.parseValue("NAME", GraphQLContext.default, Locale.default) == 42 } def "serialize returns name for value"() { + expect: + enumType.serialize(42, GraphQLContext.default, Locale.default) == "NAME" + } + + def "serialize returns name for value with deprecated method"() { expect: enumType.serialize(42) == "NAME" } def "serialize throws exception for unknown value"() { when: - enumType.serialize(12) + enumType.serialize(12, GraphQLContext.default, Locale.default) then: thrown(CoercingSerializeException) } - def "parseLiteral return null for invalid input"() { + when: + enumType.parseLiteral(StringValue.newStringValue("foo").build(), + GraphQLContext.default, + Locale.default) + then: + thrown(CoercingParseLiteralException) + } + + def "parseLiteral return null for invalid input with deprecated method"() { when: enumType.parseLiteral(StringValue.newStringValue("foo").build()) then: @@ -53,17 +74,20 @@ class GraphQLEnumTypeTest extends Specification { def "parseLiteral return null for invalid enum name"() { when: - enumType.parseLiteral(EnumValue.newEnumValue("NOT_NAME").build()) + enumType.parseLiteral(EnumValue.newEnumValue("NOT_NAME").build(), + GraphQLContext.default, + Locale.default) then: thrown(CoercingParseLiteralException) } def "parseLiteral returns value for 'NAME'"() { expect: - enumType.parseLiteral(EnumValue.newEnumValue("NAME").build()) == 42 + enumType.parseLiteral(EnumValue.newEnumValue("NAME").build(), + GraphQLContext.default, + Locale.default) == 42 } - def "null values are not allowed"() { when: newEnum().name("AnotherTestEnum") @@ -72,7 +96,6 @@ class GraphQLEnumTypeTest extends Specification { thrown(AssertException) } - def "duplicate value definition overwrites"() { when: def enumType = newEnum().name("AnotherTestEnum") @@ -96,7 +119,7 @@ class GraphQLEnumTypeTest extends Specification { .build() when: - def serialized = enumType.serialize(Episode.EMPIRE) + def serialized = enumType.serialize(Episode.EMPIRE, GraphQLContext.default, Locale.default) then: serialized == "EMPIRE" @@ -111,7 +134,7 @@ class GraphQLEnumTypeTest extends Specification { .build() when: - def serialized = enumType.serialize(Episode.NEWHOPE) + def serialized = enumType.serialize(Episode.NEWHOPE, GraphQLContext.default, Locale.default) then: serialized == "NEWHOPE" @@ -128,7 +151,7 @@ class GraphQLEnumTypeTest extends Specification { String stringInput = Episode.NEWHOPE.toString() when: - def serialized = enumType.serialize(stringInput) + def serialized = enumType.serialize(stringInput, GraphQLContext.default, Locale.default) then: serialized == "NEWHOPE" diff --git a/src/test/groovy/graphql/schema/GraphQLFieldDefinitionTest.groovy b/src/test/groovy/graphql/schema/GraphQLFieldDefinitionTest.groovy index 065ee682e8..d3c775cc9e 100644 --- a/src/test/groovy/graphql/schema/GraphQLFieldDefinitionTest.groovy +++ b/src/test/groovy/graphql/schema/GraphQLFieldDefinitionTest.groovy @@ -1,21 +1,26 @@ package graphql.schema import graphql.AssertException +import graphql.TestUtil +import graphql.schema.idl.SchemaPrinter import spock.lang.Specification import static graphql.Scalars.GraphQLBoolean import static graphql.Scalars.GraphQLFloat import static graphql.Scalars.GraphQLInt import static graphql.Scalars.GraphQLString +import static graphql.TestUtil.mockArguments +import static graphql.schema.DefaultGraphqlTypeComparatorRegistry.newComparators import static graphql.schema.GraphQLArgument.newArgument import static graphql.schema.GraphQLDirective.newDirective import static graphql.schema.GraphQLFieldDefinition.newFieldDefinition +import static graphql.schema.idl.SchemaPrinter.Options.defaultOptions class GraphQLFieldDefinitionTest extends Specification { def "dataFetcher can't be null"() { when: - newFieldDefinition().dataFetcher(null) + newFieldDefinition().dataFetcher(null) // Retain for test coverage then: def exception = thrown(AssertException) exception.getMessage().contains("dataFetcher") @@ -77,4 +82,19 @@ class GraphQLFieldDefinitionTest extends Specification { transformedField.getDirective("directive2") != null transformedField.getDirective("directive3") != null } + + def "test deprecated argument builder for list"() { + given: + def field = newFieldDefinition().name("field").type(GraphQLInt).argument(mockArguments("a", "bb")).build() // Retain for test coverage + + when: + def registry = newComparators() + .addComparator({ it.parentType(GraphQLFieldDefinition.class).elementType(GraphQLArgument.class) }, GraphQLArgument.class, TestUtil.byGreatestLength) + .build() + def options = defaultOptions().setComparators(registry) + def printer = new SchemaPrinter(options) + + then: + printer.argsString(GraphQLFieldDefinition.class, field.arguments) == '''(bb: Int, a: Int)''' + } } diff --git a/src/test/groovy/graphql/schema/GraphQLInputObjectTypeTest.groovy b/src/test/groovy/graphql/schema/GraphQLInputObjectTypeTest.groovy index fe54a55bc9..81a7d19c20 100644 --- a/src/test/groovy/graphql/schema/GraphQLInputObjectTypeTest.groovy +++ b/src/test/groovy/graphql/schema/GraphQLInputObjectTypeTest.groovy @@ -1,5 +1,9 @@ package graphql.schema +import graphql.GraphQLContext +import graphql.StarWarsSchema +import graphql.language.ObjectValue +import graphql.validation.ValidationUtil import spock.lang.Specification import static graphql.Scalars.GraphQLBoolean @@ -7,6 +11,7 @@ import static graphql.Scalars.GraphQLInt import static graphql.Scalars.GraphQLString import static graphql.schema.GraphQLInputObjectField.newInputObjectField import static graphql.schema.GraphQLInputObjectType.newInputObject +import static graphql.schema.GraphQLNonNull.nonNull class GraphQLInputObjectTypeTest extends Specification { @@ -54,4 +59,24 @@ class GraphQLInputObjectTypeTest extends Specification { transformedInputType.getFieldDefinition("Str").getType() == GraphQLBoolean } + def "deprecated default value builder works"() { + given: + def graphQLContext = GraphQLContext.getDefault() + def schema = GraphQLSchema.newSchema() + .query(StarWarsSchema.queryType) + .codeRegistry(StarWarsSchema.codeRegistry) + .build() + def validationUtil = new ValidationUtil() + def inputObjectType = GraphQLInputObjectType.newInputObject() + .name("inputObjectType") + .field(GraphQLInputObjectField.newInputObjectField() + .name("hello") + .type(nonNull(GraphQLString)) + .defaultValue("default")) // Retain deprecated builder for test coverage + .build() + def objectValue = ObjectValue.newObjectValue() + + expect: + validationUtil.isValidLiteralValue(objectValue.build(), inputObjectType, schema, graphQLContext, Locale.ENGLISH) + } } diff --git a/src/test/groovy/graphql/schema/GraphQLInterfaceTypeTest.groovy b/src/test/groovy/graphql/schema/GraphQLInterfaceTypeTest.groovy index af67996118..b6c4273ae3 100644 --- a/src/test/groovy/graphql/schema/GraphQLInterfaceTypeTest.groovy +++ b/src/test/groovy/graphql/schema/GraphQLInterfaceTypeTest.groovy @@ -24,7 +24,6 @@ class GraphQLInterfaceTypeTest extends Specification { newFieldDefinition().name("NAME").type(GraphQLString).build(), newFieldDefinition().name("NAME").type(GraphQLInt).build() ]) - .typeResolver(new TypeResolverProxy()) .build() then: interfaceType.getName() == "TestInterfaceType" @@ -37,7 +36,6 @@ class GraphQLInterfaceTypeTest extends Specification { .description("StartingDescription") .field(newFieldDefinition().name("Str").type(GraphQLString)) .field(newFieldDefinition().name("Int").type(GraphQLInt)) - .typeResolver(new TypeResolverProxy()) .build() when: @@ -107,4 +105,19 @@ class GraphQLInterfaceTypeTest extends Specification { then: noExceptionThrown() } + + def "deprecated typeResolver builder works"() { + when: + def interfaceType = newInterface().name("TestInterfaceType") + .description("description") + .fields([ + newFieldDefinition().name("NAME").type(GraphQLString).build(), + newFieldDefinition().name("NAME").type(GraphQLInt).build() + ]) + .typeResolver(new TypeResolverProxy()) // Retain for test coverage + .build() + then: + interfaceType.getName() == "TestInterfaceType" + interfaceType.getFieldDefinition("NAME").getType() == GraphQLInt + } } diff --git a/src/test/groovy/graphql/schema/GraphQLSchemaTest.groovy b/src/test/groovy/graphql/schema/GraphQLSchemaTest.groovy index d112f40fc8..3ddf718f71 100644 --- a/src/test/groovy/graphql/schema/GraphQLSchemaTest.groovy +++ b/src/test/groovy/graphql/schema/GraphQLSchemaTest.groovy @@ -106,31 +106,29 @@ class GraphQLSchemaTest extends Specification { } static def basicSchemaBuilder() { + def queryTypeName = "QueryType" + def fooCoordinates = FieldCoordinates.coordinates(queryTypeName, "hero") + DataFetcher basicDataFetcher = new DataFetcher() { + @Override + Object get(DataFetchingEnvironment environment) throws Exception { + return null + } + } + + GraphQLCodeRegistry codeRegistry = GraphQLCodeRegistry.newCodeRegistry() + .dataFetcher(fooCoordinates, basicDataFetcher) + .build() + GraphQLSchema.newSchema() + .codeRegistry(codeRegistry) .query(newObject() .name("QueryType") .field(newFieldDefinition() .name("hero") .type(GraphQLString) - .dataFetcher({ env -> null }))) + )) } - def additionalType1 = newObject() - .name("Additional1") - .field(newFieldDefinition() - .name("field") - .type(GraphQLString) - .dataFetcher({ env -> null })) - .build() - - def additionalType2 = newObject() - .name("Additional2") - .field(newFieldDefinition() - .name("field") - .type(GraphQLString) - .dataFetcher({ env -> null })) - .build() - def "clear directives works as expected"() { setup: def schemaBuilder = basicSchemaBuilder() @@ -156,7 +154,7 @@ class GraphQLSchemaTest extends Specification { schema.directives.size() == 4 } - def "clear additional types works as expected"() { + def "clear additional types works as expected"() { setup: def schemaBuilder = basicSchemaBuilder() @@ -171,12 +169,47 @@ class GraphQLSchemaTest extends Specification { schema.additionalTypes.empty when: "clear types is called with additional types" - schema = schemaBuilder.clearAdditionalTypes().additionalType(additionalType1).build() + def additional1TypeName = "Additional1" + def additional2TypeName = "Additional2" + def fieldName = "field" + def additionalType1 = newObject() + .name(additional1TypeName) + .field(newFieldDefinition() + .name(fieldName) + .type(GraphQLString)) + .build() + def additionalType2 = newObject() + .name("Additional2") + .field(newFieldDefinition() + .name(fieldName) + .type(GraphQLString)) + .build() + + def additional1Coordinates = FieldCoordinates.coordinates(additionalType1, fieldName) + DataFetcher basicDataFetcher = new DataFetcher() { + @Override + Object get(DataFetchingEnvironment environment) throws Exception { + return null + } + } + + GraphQLCodeRegistry codeRegistry = GraphQLCodeRegistry.newCodeRegistry() + .dataFetcher(additional1Coordinates, basicDataFetcher) + .build() + + schema = schemaBuilder + .clearAdditionalTypes() + .additionalType(additionalType1) + .codeRegistry(codeRegistry) + .build() + then: schema.additionalTypes.size() == 1 when: "the schema is transformed, things are copied" - schema = schema.transform({ builder -> builder.additionalType(additionalType2) }) + def additional2Coordinates = FieldCoordinates.coordinates(additional2TypeName, fieldName) + codeRegistry = codeRegistry.transform({ builder -> builder.dataFetcher(additional2Coordinates, basicDataFetcher) }) + schema = schema.transform({ builder -> builder.additionalType(additionalType2).codeRegistry(codeRegistry) }) then: schema.additionalTypes.size() == 2 } @@ -294,7 +327,7 @@ class GraphQLSchemaTest extends Specification { it.replaceFields(fields) }) - return changeNode(context, newObjectType); + return changeNode(context, newObjectType) } return TraversalControl.CONTINUE } diff --git a/src/test/groovy/graphql/schema/GraphQLUnionTypeTest.groovy b/src/test/groovy/graphql/schema/GraphQLUnionTypeTest.groovy index e537a8766b..6524d135da 100644 --- a/src/test/groovy/graphql/schema/GraphQLUnionTypeTest.groovy +++ b/src/test/groovy/graphql/schema/GraphQLUnionTypeTest.groovy @@ -14,7 +14,6 @@ class GraphQLUnionTypeTest extends Specification { when: newUnionType() .name("TestUnionType") - .typeResolver(new TypeResolverProxy()) .build() then: thrown(AssertException) @@ -38,7 +37,6 @@ class GraphQLUnionTypeTest extends Specification { .description("StartingDescription") .possibleType(objType1) .possibleType(objType2) - .typeResolver(new TypeResolverProxy()) .build() when: @@ -65,4 +63,13 @@ class GraphQLUnionTypeTest extends Specification { transformedUnion.isPossibleType(objType3) } + def "deprecated typeResolver builder works"() { + when: + newUnionType() + .name("TestUnionType") + .typeResolver(new TypeResolverProxy()) // Retain for test coverage + .build() + then: + thrown(AssertException) + } } diff --git a/src/test/groovy/graphql/schema/PropertyDataFetcherTest.groovy b/src/test/groovy/graphql/schema/PropertyDataFetcherTest.groovy index 523f0e0dad..3d289c3688 100644 --- a/src/test/groovy/graphql/schema/PropertyDataFetcherTest.groovy +++ b/src/test/groovy/graphql/schema/PropertyDataFetcherTest.groovy @@ -2,9 +2,12 @@ package graphql.schema import graphql.ExecutionInput import graphql.TestUtil +import graphql.schema.fetching.ConfusedPojo import graphql.schema.somepackage.ClassWithDFEMethods import graphql.schema.somepackage.ClassWithInterfaces import graphql.schema.somepackage.ClassWithInteritanceAndInterfaces +import graphql.schema.somepackage.RecordLikeClass +import graphql.schema.somepackage.RecordLikeTwoClassesDown import graphql.schema.somepackage.TestClass import graphql.schema.somepackage.TwoClassesDown import spock.lang.Specification @@ -20,6 +23,7 @@ class PropertyDataFetcherTest extends Specification { PropertyDataFetcher.setUseSetAccessible(true) PropertyDataFetcher.setUseNegativeCache(true) PropertyDataFetcher.clearReflectionCache() + PropertyDataFetcherHelper.setUseLambdaFactory(true) } def env(obj) { @@ -95,6 +99,91 @@ class PropertyDataFetcherTest extends Specification { result == null } + def "fetch via record method"() { + def environment = env(new RecordLikeClass()) + when: + def fetcher = new PropertyDataFetcher("recordProperty") + def result = fetcher.get(environment) + then: + result == "recordProperty" + + // caching works + when: + fetcher = new PropertyDataFetcher("recordProperty") + result = fetcher.get(environment) + then: + result == "recordProperty" + + // recordArgumentMethod will not work because it takes a parameter + when: + fetcher = new PropertyDataFetcher("recordArgumentMethod") + result = fetcher.get(environment) + then: + result == null + + // equals will not work because it takes a parameter + when: + fetcher = new PropertyDataFetcher("equals") + result = fetcher.get(environment) + then: + result == null + + // we allow hashCode() and toString() because why not - they are valid property names + // they might not be that useful but they can be accessed + + when: + fetcher = new PropertyDataFetcher("hashCode") + result = fetcher.get(environment) + then: + result == 666 + + when: + fetcher = new PropertyDataFetcher("toString") + result = fetcher.get(environment) + then: + result == "toString" + } + + def "can fetch record like methods that are public and on super classes"() { + def environment = env(new RecordLikeTwoClassesDown()) + when: + def fetcher = new PropertyDataFetcher("recordProperty") + def result = fetcher.get(environment) + then: + result == "recordProperty" + } + + def "fetch via record method without lambda support"() { + PropertyDataFetcherHelper.setUseLambdaFactory(false) + PropertyDataFetcherHelper.clearReflectionCache() + + when: + def environment = env(new RecordLikeClass()) + def fetcher = new PropertyDataFetcher("recordProperty") + def result = fetcher.get(environment) + then: + result == "recordProperty" + + when: + environment = env(new RecordLikeTwoClassesDown()) + fetcher = new PropertyDataFetcher("recordProperty") + result = fetcher.get(environment) + then: + result == "recordProperty" + } + + def "fetch via record method without lambda support in preference to getter methods"() { + PropertyDataFetcherHelper.setUseLambdaFactory(false) + PropertyDataFetcherHelper.clearReflectionCache() + + when: + def environment = env(new ConfusedPojo()) + def fetcher = new PropertyDataFetcher("recordLike") + def result = fetcher.get(environment) + then: + result == "recordLike" + } + def "fetch via public method"() { def environment = env(new TestClass()) def fetcher = new PropertyDataFetcher("publicProperty") @@ -106,9 +195,16 @@ class PropertyDataFetcherTest extends Specification { def "fetch via public method declared two classes up"() { def environment = env(new TwoClassesDown("aValue")) def fetcher = new PropertyDataFetcher("publicProperty") + when: def result = fetcher.get(environment) - expect: + then: result == "publicValue" + + when: + result = fetcher.get(environment) + then: + result == "publicValue" + } def "fetch via property only defined on package protected impl"() { diff --git a/src/test/groovy/graphql/schema/SchemaPrinterComparatorsTest.groovy b/src/test/groovy/graphql/schema/SchemaPrinterComparatorsTest.groovy index 6ddb0bc04c..f2a12e3b7e 100644 --- a/src/test/groovy/graphql/schema/SchemaPrinterComparatorsTest.groovy +++ b/src/test/groovy/graphql/schema/SchemaPrinterComparatorsTest.groovy @@ -534,7 +534,7 @@ scalar TestScalar @bb(bb : 0, a : 0) @a(bb : 0, a : 0) def "argsString uses most specific registered comparators"() { given: - def field = newFieldDefinition().name("field").type(GraphQLInt).argument(mockArguments("a", "bb")).build() + def field = newFieldDefinition().name("field").type(GraphQLInt).arguments(mockArguments("a", "bb")).build() when: def registry = newComparators() @@ -549,7 +549,7 @@ scalar TestScalar @bb(bb : 0, a : 0) @a(bb : 0, a : 0) def "argsString uses least specific registered comparators"() { given: - def field = newFieldDefinition().name("field").type(GraphQLInt).argument(mockArguments("a", "bb")).build() + def field = newFieldDefinition().name("field").type(GraphQLInt).arguments(mockArguments("a", "bb")).build() when: def registry = newComparators() diff --git a/src/test/groovy/graphql/schema/SchemaTransformerTest.groovy b/src/test/groovy/graphql/schema/SchemaTransformerTest.groovy index 4d41f672b2..6922852633 100644 --- a/src/test/groovy/graphql/schema/SchemaTransformerTest.groovy +++ b/src/test/groovy/graphql/schema/SchemaTransformerTest.groovy @@ -28,7 +28,7 @@ class SchemaTransformerTest extends Specification { bar: String } """) - schema.getQueryType(); + schema.getQueryType() SchemaTransformer schemaTransformer = new SchemaTransformer() when: GraphQLSchema newSchema = schemaTransformer.transform(schema, new GraphQLTypeVisitorStub() { @@ -39,7 +39,7 @@ class SchemaTransformerTest extends Specification { def changedNode = fieldDefinition.transform({ builder -> builder.name("barChanged") }) return changeNode(context, changedNode) } - return TraversalControl.CONTINUE; + return TraversalControl.CONTINUE } }) @@ -59,7 +59,7 @@ class SchemaTransformerTest extends Specification { baz: String } """) - schema.getQueryType(); + schema.getQueryType() SchemaTransformer schemaTransformer = new SchemaTransformer() when: GraphQLSchema newSchema = schemaTransformer.transform(schema, new GraphQLTypeVisitorStub() { @@ -69,7 +69,7 @@ class SchemaTransformerTest extends Specification { if (fieldDefinition.name == "baz") { return deleteNode(context) } - return TraversalControl.CONTINUE; + return TraversalControl.CONTINUE } }) @@ -133,7 +133,7 @@ class SchemaTransformerTest extends Specification { baz: String } """) - schema.getQueryType(); + schema.getQueryType() SchemaTransformer schemaTransformer = new SchemaTransformer() when: GraphQLSchema newSchema = schemaTransformer.transform(schema, new GraphQLTypeVisitorStub() { @@ -143,7 +143,7 @@ class SchemaTransformerTest extends Specification { if (fieldDefinition.name == "baz") { return deleteNode(context) } - return TraversalControl.CONTINUE; + return TraversalControl.CONTINUE } }) @@ -182,7 +182,7 @@ class SchemaTransformerTest extends Specification { def changedNode = fieldDefinition.transform({ builder -> builder.name("helloChanged") }) return changeNode(context, changedNode) } - return TraversalControl.CONTINUE; + return TraversalControl.CONTINUE } @Override @@ -201,7 +201,7 @@ class SchemaTransformerTest extends Specification { @Override TraversalControl visitGraphQLTypeReference(GraphQLTypeReference node, TraverserContext context) { if (node.name == "Parent") { - return changeNode(context, typeRef("ParentChanged")); + return changeNode(context, typeRef("ParentChanged")) } return super.visitGraphQLTypeReference(node, context) } @@ -303,21 +303,30 @@ type SubChildChanged { def queryObject = newObject() .name("Query") .field({ builder -> - builder.name("foo").type(Scalars.GraphQLString).dataFetcher(new DataFetcher() { - @Override - Object get(DataFetchingEnvironment environment) throws Exception { - return "bar"; - } - }) - }).build(); + builder.name("foo") + .type(Scalars.GraphQLString) + }).build() + + def fooCoordinates = FieldCoordinates.coordinates("Query", "foo") + DataFetcher dataFetcher = new DataFetcher() { + @Override + Object get(DataFetchingEnvironment environment) throws Exception { + return "bar" + } + } + GraphQLCodeRegistry codeRegistry = GraphQLCodeRegistry.newCodeRegistry() + .dataFetcher(fooCoordinates, dataFetcher) + .build() def schemaObject = newSchema() + .codeRegistry(codeRegistry) .query(queryObject) .build() when: def result = GraphQL.newGraphQL(schemaObject) - .build().execute(''' + .build() + .execute(''' { foo } ''').getData() @@ -336,14 +345,20 @@ type SubChildChanged { return TraversalControl.CONTINUE } }) + + def fooTransformedCoordinates = FieldCoordinates.coordinates("Query", "fooChanged") + codeRegistry = codeRegistry.transform({it.dataFetcher(fooTransformedCoordinates, dataFetcher)}) + newSchema = newSchema.transform({ + builder -> builder.codeRegistry(codeRegistry) + }) result = GraphQL.newGraphQL(newSchema) - .build().execute(''' + .build() + .execute(''' { fooChanged } ''').getData() then: (result as Map)['fooChanged'] == 'bar' - } def "transformed schema can be executed"() { @@ -430,7 +445,7 @@ type SubChildChanged { if (fieldDefinition.name == "billingStatus") { return deleteNode(context) } - return TraversalControl.CONTINUE; + return TraversalControl.CONTINUE } }) @@ -448,11 +463,11 @@ type SubChildChanged { @Override TraversalControl visitGraphQLDirective(GraphQLDirective node, TraverserContext context) { - if ("internalnote".equals(node.getName())) { + if ("internalnote" == node.getName()) { // this deletes the declaration and the two usages of it - deleteNode(context); + deleteNode(context) } - return TraversalControl.CONTINUE; + return TraversalControl.CONTINUE } } @@ -628,7 +643,7 @@ type Query { @Override TraversalControl visitGraphQLObjectType(GraphQLObjectType node, TraverserContext context) { - if (node.getName().startsWith("__")) return TraversalControl.ABORT; + if (node.getName().startsWith("__")) return TraversalControl.ABORT node = node.transform({ b -> b.name(node.getName().toUpperCase()) }) return changeNode(context, node) } @@ -716,7 +731,7 @@ type Query { @Override TraversalControl visitGraphQLObjectType(GraphQLObjectType node, TraverserContext context) { - if (node.getName().equals('ToDel')) { + if (node.getName() == 'ToDel') { return deleteNode(context) } return TraversalControl.CONTINUE @@ -774,7 +789,7 @@ type Query { if (node.name == "__Field") { return changeNode(context, node.transform({ it.name("__FieldChanged") })) } - return TraversalControl.CONTINUE; + return TraversalControl.CONTINUE } } @@ -841,4 +856,79 @@ type Query { } ''' } + + def "can rename scalars"() { + + def schema = TestUtil.schema(""" + scalar Foo + type Query { + field : Foo + } +""") + + def visitor = new GraphQLTypeVisitorStub() { + + @Override + TraversalControl visitGraphQLScalarType(GraphQLScalarType node, TraverserContext context) { + if (node.getName() == "Foo") { + GraphQLScalarType newNode = node.transform({sc -> sc.name("Bar")}) + return changeNode(context, newNode) + } + return super.visitGraphQLScalarType(node, context) + } + } + + when: + def newSchema = SchemaTransformer.transformSchema(schema, visitor) + then: + newSchema.getType("Bar") instanceof GraphQLScalarType + newSchema.getType("Foo") == null + (newSchema.getObjectType("Query").getFieldDefinition("field").getType() as GraphQLScalarType).getName() == "Bar" + } + + def "rename scalars are changed in applied arguments"() { + + def schema = TestUtil.schema(""" + scalar Foo + directive @myDirective(fooArgOnDirective: Foo) on FIELD_DEFINITION + type Query { + foo(fooArgOnField: Foo) : Foo @myDirective + } +""") + + def visitor = new GraphQLTypeVisitorStub() { + + @Override + TraversalControl visitGraphQLScalarType(GraphQLScalarType node, TraverserContext context) { + if (node.getName() == "Foo") { + GraphQLScalarType newNode = node.transform({sc -> sc.name("Bar")}) + return changeNode(context, newNode) + } + return super.visitGraphQLScalarType(node, context) + } + } + + when: + def newSchema = SchemaTransformer.transformSchema(schema, visitor) + then: + + def fieldDef = newSchema.getObjectType("Query").getFieldDefinition("foo") + def appliedDirective = fieldDef.getAppliedDirective("myDirective") + def oldSkoolDirective = fieldDef.getDirective("myDirective") + def argument = fieldDef.getArgument("fooArgOnField") + def directiveDecl = newSchema.getDirective("myDirective") + def directiveArgument = directiveDecl.getArgument("fooArgOnDirective") + + (fieldDef.getType() as GraphQLScalarType).getName() == "Bar" + (argument.getType() as GraphQLScalarType).getName() == "Bar" + (directiveArgument.getType() as GraphQLScalarType).getName() == "Bar" + + (oldSkoolDirective.getArgument("fooArgOnDirective").getType() as GraphQLScalarType).getName() == "Bar" + + newSchema.getType("Bar") instanceof GraphQLScalarType + + // not working at this stage + (appliedDirective.getArgument("fooArgOnDirective").getType() as GraphQLScalarType).getName() == "Bar" + newSchema.getType("Foo") == null + } } diff --git a/src/test/groovy/graphql/schema/SchemaTraverserTest.groovy b/src/test/groovy/graphql/schema/SchemaTraverserTest.groovy index ea6deb208e..889e7e2198 100644 --- a/src/test/groovy/graphql/schema/SchemaTraverserTest.groovy +++ b/src/test/groovy/graphql/schema/SchemaTraverserTest.groovy @@ -1,7 +1,6 @@ package graphql.schema import graphql.Scalars -import graphql.TypeResolutionEnvironment import graphql.util.TraversalControl import graphql.util.TraverserContext import spock.lang.Specification @@ -117,7 +116,6 @@ class SchemaTraverserTest extends Specification { .name("bar") .type(Scalars.GraphQLString) .build()) - .typeResolver(NOOP_RESOLVER) .build()) then: visitor.getStack() == ["interface: foo", "fallback: foo", @@ -155,7 +153,6 @@ class SchemaTraverserTest extends Specification { .build()) .withInterface(GraphQLInterfaceType.newInterface() .name("bar") - .typeResolver(NOOP_RESOLVER) .build()) .build()) then: @@ -181,7 +178,6 @@ class SchemaTraverserTest extends Specification { .name("foo") .possibleType(GraphQLObjectType.newObject().name("dummy").build()) .possibleType(typeRef("dummyRef")) - .typeResolver(NOOP_RESOLVER) .build()) then: visitor.getStack() == ["union: foo", "fallback: foo", @@ -193,21 +189,21 @@ class SchemaTraverserTest extends Specification { when: def visitor = new GraphQLTestingVisitor() def coercing = new Coercing() { - private static final String TEST_ONLY = "For testing only"; + private static final String TEST_ONLY = "For testing only" @Override Object serialize(Object dataFetcherResult) throws CoercingSerializeException { - throw new UnsupportedOperationException(TEST_ONLY); + throw new UnsupportedOperationException(TEST_ONLY) } @Override Object parseValue(Object input) throws CoercingParseValueException { - throw new UnsupportedOperationException(TEST_ONLY); + throw new UnsupportedOperationException(TEST_ONLY) } @Override Object parseLiteral(Object input) throws CoercingParseLiteralException { - throw new UnsupportedOperationException(TEST_ONLY); + throw new UnsupportedOperationException(TEST_ONLY) } } def scalarType = GraphQLScalarType.newScalar() @@ -283,7 +279,6 @@ class SchemaTraverserTest extends Specification { def visitor = new GraphQLTestingVisitor() def interfaceType = GraphQLInterfaceType.newInterface() .name("foo") - .typeResolver(NOOP_RESOLVER) .withDirective(GraphQLDirective.newDirective() .name("bar")) .withAppliedDirective(GraphQLAppliedDirective.newDirective() @@ -302,7 +297,6 @@ class SchemaTraverserTest extends Specification { def unionType = GraphQLUnionType.newUnionType() .name("foo") .possibleType(GraphQLObjectType.newObject().name("dummy").build()) - .typeResolver(NOOP_RESOLVER) .withDirective(GraphQLDirective.newDirective() .name("bar")) .build() @@ -388,15 +382,6 @@ class SchemaTraverserTest extends Specification { } - - def NOOP_RESOLVER = new TypeResolver() { - @Override - GraphQLObjectType getType(TypeResolutionEnvironment env) { - return null - } - } - - class GraphQLTestingVisitor extends GraphQLTypeVisitorStub { def stack = [] diff --git a/src/test/groovy/graphql/schema/diff/SchemaDiffTest.groovy b/src/test/groovy/graphql/schema/diff/SchemaDiffTest.groovy index ca957db197..b945abc143 100644 --- a/src/test/groovy/graphql/schema/diff/SchemaDiffTest.groovy +++ b/src/test/groovy/graphql/schema/diff/SchemaDiffTest.groovy @@ -62,31 +62,31 @@ class SchemaDiffTest extends Specification { return RuntimeWiring.newRuntimeWiring() .wiringFactory(new WiringFactory() { - @Override - boolean providesTypeResolver(UnionWiringEnvironment environment) { - return true - } - - @Override - boolean providesTypeResolver(InterfaceWiringEnvironment environment) { - return true - } - - @Override - TypeResolver getTypeResolver(InterfaceWiringEnvironment environment) { - return NULL_TYPE_RESOLVER - } - - @Override - TypeResolver getTypeResolver(UnionWiringEnvironment environment) { - return NULL_TYPE_RESOLVER - } - - @Override - DataFetcher getDefaultDataFetcher(FieldWiringEnvironment environment) { - return new PropertyDataFetcher(environment.getFieldDefinition().getName()) - } - }) + @Override + boolean providesTypeResolver(UnionWiringEnvironment environment) { + return true + } + + @Override + boolean providesTypeResolver(InterfaceWiringEnvironment environment) { + return true + } + + @Override + TypeResolver getTypeResolver(InterfaceWiringEnvironment environment) { + return NULL_TYPE_RESOLVER + } + + @Override + TypeResolver getTypeResolver(UnionWiringEnvironment environment) { + return NULL_TYPE_RESOLVER + } + + @Override + DataFetcher getDefaultDataFetcher(FieldWiringEnvironment environment) { + return new PropertyDataFetcher(environment.getFieldDefinition().getName()) + } + }) .scalar(CUSTOM_SCALAR) .build() } @@ -105,7 +105,7 @@ class SchemaDiffTest extends Specification { } - def "change_in_null_ness"() { + def "change_in_null_ness_input_or_arg"() { given: Type baseLine = new NonNullType(new ListType(new TypeName("foo"))) @@ -116,12 +116,12 @@ class SchemaDiffTest extends Specification { def diff = new SchemaDiff() - def sameType = diff.checkTypeWithNonNullAndList(baseLine, same) + def sameType = diff.checkTypeWithNonNullAndListOnInputOrArg(baseLine, same) - def lessStrict = diff.checkTypeWithNonNullAndList(baseLine, less) + def lessStrict = diff.checkTypeWithNonNullAndListOnInputOrArg(baseLine, less) // not allowed as old clients wont work - def moreStrict = diff.checkTypeWithNonNullAndList(less, baseLine) + def moreStrict = diff.checkTypeWithNonNullAndListOnInputOrArg(less, baseLine) expect: @@ -130,18 +130,60 @@ class SchemaDiffTest extends Specification { moreStrict == DiffCategory.STRICTER } - def "change_in_list_ness"() { + def "change_in_null_ness_object_or_interface"() { given: - Type baseLine = new NonNullType(new ListType(new TypeName("foo"))) + Type nonNull = new NonNullType(new ListType(new TypeName("foo"))) + Type nonNullDuplicate = new NonNullType(new ListType(new TypeName("foo"))) + + Type nullable = new ListType(new TypeName("foo")) + + + def diff = new SchemaDiff() + + def sameType = diff.checkTypeWithNonNullAndListOnObjectOrInterface(nonNull, nonNullDuplicate) + + def removeGuarantee = diff.checkTypeWithNonNullAndListOnObjectOrInterface(nonNull, nullable) + + def addGuarantee = diff.checkTypeWithNonNullAndListOnObjectOrInterface(nullable, nonNull) + + + expect: + sameType == null + removeGuarantee == DiffCategory.STRICTER + addGuarantee == null + } + + def "change_in_list_ness_input_or_arg"() { + + given: + Type list = new NonNullType(new ListType(new TypeName("foo"))) + Type notList = new NonNullType(new TypeName("foo")) + + def diff = new SchemaDiff() + + def noLongerList = diff.checkTypeWithNonNullAndListOnInputOrArg(list, notList) + def nowList = diff.checkTypeWithNonNullAndListOnInputOrArg(notList, list) + + expect: + noLongerList == DiffCategory.INVALID + nowList == DiffCategory.INVALID + } + + def "change_in_list_ness_object_or_interface"() { + + given: + Type list = new NonNullType(new ListType(new TypeName("foo"))) Type notList = new NonNullType(new TypeName("foo")) def diff = new SchemaDiff() - def noLongerList = diff.checkTypeWithNonNullAndList(baseLine, notList) + def noLongerList = diff.checkTypeWithNonNullAndListOnObjectOrInterface(list, notList) + def nowList = diff.checkTypeWithNonNullAndListOnObjectOrInterface(list, notList) expect: noLongerList == DiffCategory.INVALID + nowList == DiffCategory.INVALID } DiffEvent lastBreakage(CapturingReporter capturingReporter) { @@ -238,7 +280,7 @@ class SchemaDiffTest extends Specification { reporter.breakageCount == 0 List newFieldEvents = reporter.infos.stream() - .filter{de -> de.typeName == "Ainur" && de.fieldName == "surname"} + .filter { de -> de.typeName == "Ainur" && de.fieldName == "surname" } .collect(Collectors.toList()) newFieldEvents.size() == 2 @@ -344,16 +386,26 @@ class SchemaDiffTest extends Specification { diff.diffSchema(diffSet, chainedReporter) expect: - reporter.breakageCount == 2 - reporter.breakages[0].category == DiffCategory.INVALID - reporter.breakages[0].typeName == 'Questor' - reporter.breakages[0].typeKind == TypeKind.InputObject - reporter.breakages[0].fieldName == 'queryTarget' + reporter.breakageCount == 4 + reporter.breakages[0].category == DiffCategory.STRICTER + reporter.breakages[0].typeName == 'Query' + reporter.breakages[0].typeKind == TypeKind.Object + reporter.breakages[0].fieldName == 'being' reporter.breakages[1].category == DiffCategory.STRICTER reporter.breakages[1].typeName == 'Questor' reporter.breakages[1].typeKind == TypeKind.InputObject - reporter.breakages[1].fieldName == 'newMandatoryField' + reporter.breakages[1].fieldName == 'nestedInput' + + reporter.breakages[2].category == DiffCategory.INVALID + reporter.breakages[2].typeName == 'Questor' + reporter.breakages[2].typeKind == TypeKind.InputObject + reporter.breakages[2].fieldName == 'queryTarget' + + reporter.breakages[3].category == DiffCategory.STRICTER + reporter.breakages[3].typeName == 'Questor' + reporter.breakages[3].typeKind == TypeKind.InputObject + reporter.breakages[3].fieldName == 'newMandatoryField' } @@ -433,27 +485,21 @@ class SchemaDiffTest extends Specification { diff.diffSchema(diffSet, chainedReporter) expect: - reporter.breakageCount == 4 + reporter.breakageCount == 3 reporter.breakages[0].category == DiffCategory.STRICTER - reporter.breakages[0].typeName == 'Query' + reporter.breakages[0].typeName == 'Istari' reporter.breakages[0].typeKind == TypeKind.Object - reporter.breakages[0].fieldName == 'being' + reporter.breakages[0].fieldName == 'temperament' reporter.breakages[1].category == DiffCategory.INVALID reporter.breakages[1].typeName == 'Query' reporter.breakages[1].typeKind == TypeKind.Object reporter.breakages[1].fieldName == 'beings' - reporter.breakages[2].category == DiffCategory.STRICTER + reporter.breakages[2].category == DiffCategory.INVALID reporter.breakages[2].typeName == 'Query' reporter.breakages[2].typeKind == TypeKind.Object reporter.breakages[2].fieldName == 'customScalar' - - reporter.breakages[3].category == DiffCategory.STRICTER - reporter.breakages[3].typeName == 'Query' - reporter.breakages[3].typeKind == TypeKind.Object - reporter.breakages[3].fieldName == 'wizards' - } def "dangerous changes"() { @@ -504,7 +550,7 @@ class SchemaDiffTest extends Specification { diff.diffSchema(diffSet, chainedReporter) expect: - reporter.dangerCount == 13 + reporter.dangerCount == 14 reporter.breakageCount == 0 reporter.dangers.every { it.getCategory() == DiffCategory.DEPRECATION_ADDED @@ -523,7 +569,7 @@ class SchemaDiffTest extends Specification { expect: reporter.dangerCount == 0 - reporter.breakageCount == 11 + reporter.breakageCount == 12 reporter.breakages.every { it.getCategory() == DiffCategory.DEPRECATION_REMOVED } @@ -570,4 +616,87 @@ class SchemaDiffTest extends Specification { } + def "field renamed"() { + def oldSchema = TestUtil.schema(''' + type Query { + hello: String + } + ''') + def newSchema = TestUtil.schema(''' + type Query { + hello2: String + } + ''') + def reporter = new CapturingReporter() + DiffSet diffSet = DiffSet.diffSet(oldSchema, newSchema) + def diff = new SchemaDiff() + when: + diff.diffSchema(diffSet, reporter) + + then: + // the old hello field is missing + reporter.breakageCount == 1 + reporter.breakages.every { + it.getCategory() == DiffCategory.MISSING + } + + } + def "interface renamed"() { + def oldSchema = TestUtil.schema(''' + type Query implements Hello{ + hello: String + world: World + } + type World implements Hello { + hello: String + } + interface Hello { + hello: String + } + ''') + def newSchema = TestUtil.schema(''' + type Query implements Hello2{ + hello: String + world: World + } + type World implements Hello2 { + hello: String + } + interface Hello2 { + hello: String + } + ''') + def reporter = new CapturingReporter() + DiffSet diffSet = DiffSet.diffSet(oldSchema, newSchema) + def diff = new SchemaDiff() + when: + diff.diffSchema(diffSet, reporter) + + then: + // two breakages for World and Query not implementing Hello anymore + reporter.breakageCount == 2 + + } + + def "SchemaDiff and CapturingReporter have the same diff counts"() { + def schema1 = TestUtil.schema("type Query { f : String }") + def schema2 = TestUtil.schema("type Query { f : Int }") + + when: + def capturingReporter = new CapturingReporter() + def schemaDiff = new SchemaDiff() + def breakingCount = schemaDiff.diffSchema(DiffSet.diffSet(schema1, schema1), capturingReporter) + then: + breakingCount == capturingReporter.getBreakageCount() + breakingCount == 0 + + when: + capturingReporter = new CapturingReporter() + schemaDiff = new SchemaDiff() + breakingCount = schemaDiff.diffSchema(DiffSet.diffSet(schema1, schema2), capturingReporter) + + then: + breakingCount == capturingReporter.getBreakageCount() + breakingCount == 1 + } } diff --git a/src/test/groovy/graphql/schema/diffing/SchemaDiffingTest.groovy b/src/test/groovy/graphql/schema/diffing/SchemaDiffingTest.groovy new file mode 100644 index 0000000000..8f55d7bc12 --- /dev/null +++ b/src/test/groovy/graphql/schema/diffing/SchemaDiffingTest.groovy @@ -0,0 +1,1530 @@ +package graphql.schema.diffing + +import graphql.TestUtil +import graphql.schema.GraphQLFieldDefinition +import graphql.schema.GraphQLSchemaElement +import graphql.schema.GraphQLTypeVisitorStub +import graphql.schema.SchemaTransformer +import graphql.util.TraversalControl +import graphql.util.TraverserContext +import spock.lang.Specification + +import static graphql.TestUtil.schema + +class SchemaDiffingTest extends Specification { + + + def "test schema generation"() { + given: + def schema = schema(""" + type Query { + hello: String + } + """) + + when: + def schemaGraph = new SchemaGraphFactory().createGraph(schema) + + then: + schemaGraph.getVerticesByType().keySet().size() == 8 + schemaGraph.getVerticesByType(SchemaGraph.SCHEMA).size() == 1 + schemaGraph.getVerticesByType(SchemaGraph.OBJECT).size() == 7 + schemaGraph.getVerticesByType(SchemaGraph.ENUM).size() == 2 + schemaGraph.getVerticesByType(SchemaGraph.ENUM_VALUE).size() == 27 + schemaGraph.getVerticesByType(SchemaGraph.INTERFACE).size() == 0 + schemaGraph.getVerticesByType(SchemaGraph.UNION).size() == 0 + schemaGraph.getVerticesByType(SchemaGraph.SCALAR).size() == 2 + schemaGraph.getVerticesByType(SchemaGraph.FIELD).size() == 39 + schemaGraph.getVerticesByType(SchemaGraph.ARGUMENT).size() == 9 + schemaGraph.getVerticesByType(SchemaGraph.INPUT_FIELD).size() == 0 + schemaGraph.getVerticesByType(SchemaGraph.INPUT_OBJECT).size() == 0 + schemaGraph.getVerticesByType(SchemaGraph.DIRECTIVE).size() == 4 + schemaGraph.getVerticesByType(SchemaGraph.APPLIED_ARGUMENT).size() == 0 + schemaGraph.getVerticesByType(SchemaGraph.APPLIED_DIRECTIVE).size() == 0 + schemaGraph.size() == 91 + + } + + def "test rename field"() { + given: + def schema1 = schema(""" + type Query { + hello: String + } + """) + def schema2 = schema(""" + type Query { + hello2: String + } + """) + + when: + def diff = new SchemaDiffing().diffGraphQLSchema(schema1, schema2) + diff.each { println it } + then: + diff.size() == 1 + + } + + def "test rename field 2"() { + given: + def schema1 = schema(""" + type Query { + fixed: String + hello: String + f3(arg3: String): String + } + type O1 { + f1(arg1: String, x: String): String + } + + """) + def schema2 = schema(""" + type Query { + hello2: String + fixed: String + f3(arg4: String): String + } + type O2 { + f2(arg2: String, y: String): String + } + """) + + when: + def diff = new SchemaDiffing().diffGraphQLSchema(schema1, schema2) + diff.each { println it } + then: + diff.size() == 6 + + } + + def "adding fields and rename and delete"() { + given: + def schema1 = schema(""" + type Query { + hello: String + toDelete:String + newField: String + newField2: String + } + type Mutation { + unchanged: Boolean + unchanged2: Other + } + type Other { + id: ID + } + """) + def schema2 = schema(""" + type Query { + helloRenamed: String + newField: String + newField2: String + } + type Mutation { + unchanged: Boolean + unchanged2: Other + } + type Other { + id: ID + } + """) + + when: + def diff = new SchemaDiffing().diffGraphQLSchema(schema1, schema2) + diff.each { println it } + then: + diff.size() == 4 + + } + + def "remove field and rename type"() { + given: + def schema1 = schema(""" + type Query { + foo: Foo + } + type Foo { + bar: Bar + toDelete:String + } + type Bar { + id: ID + name: String + } + """) + def schema2 = schema(""" + type Query { + foo: FooRenamed + } + type FooRenamed { + bar: Bar + } + type Bar { + id: ID + name: String + } + """) + + when: + def diff = new SchemaDiffing().diffGraphQLSchema(schema1, schema2) + diff.each { println it } + then: + diff.size() == 5 + + } + + def "renamed field and added field and type"() { + given: + def schema1 = schema(""" + type Query { + foo: Foo + } + type Foo { + foo:String + } + """) + def schema2 = schema(""" + type Query { + foo: Foo + } + type Foo { + fooRenamed:String + bar: Bar + } + type Bar { + id: String + name: String + } + """) + + when: + def diff = new SchemaDiffing().diffGraphQLSchema(schema1, schema2) + diff.each { println it } + then: + /** + * 1: Changed Field + * 2: New Object + * 3-8: Three new Fields + DummyTypes + * 9-17: Edges from Object to new Fields (3) + Edges from Field to Dummy Type (3) + Edges from DummyType to String + * */ + diff.size() == 11 + + } + + + def "test two field renames one type rename"() { + given: + def schema1 = schema(""" + type Query { + hello: Foo + } + type Foo { + foo: String + } + """) + def schema2 = schema(""" + type Query { + hello2: Foo2 + } + type Foo2 { + foo2: String + } + """) + + when: + def diff = new SchemaDiffing().diffGraphQLSchema(schema1, schema2) + + then: + diff.size() == 4 + + } + + def "test field type change"() { + given: + def schema1 = schema(""" + type Query { + hello: Boolean + } + """) + def schema2 = schema(""" + type Query { + hello: String + } + """) + + when: + def diff = new SchemaDiffing().diffGraphQLSchema(schema1, schema2) + + then: + /** + * Deleting the edge from __DUMMY_TYPE_VERTICE to Boolean + * Insert the edge from __DUMMY_TYPE_VERTICE to String + */ + diff.size() == 2 + + } + + def "change object type name used once"() { + given: + def schema1 = schema(""" + type Query { + hello: Foo + } + type Foo { + foo: String + } + """) + def schema2 = schema(""" + type Query { + hello: Foo2 + } + type Foo2 { + foo: String + } + """) + + when: + def diff = new SchemaDiffing().diffGraphQLSchema(schema1, schema2) + + then: + diff.size() == 2 + + } + + def "remove Interface from Object"() { + given: + def schema1 = schema(""" + type Query { + hello: Foo + hello2: Foo2 + } + interface Node { + id: ID + } + type Foo implements Node{ + id: ID + } + type Foo2 implements Node{ + id: ID + } + """) + def schema2 = schema(""" + type Query { + hello: Foo + hello2: Foo2 + } + interface Node { + id: ID + } + type Foo implements Node{ + id: ID + } + type Foo2 { + id: ID + } + """) + + when: + def diff = new SchemaDiffing().diffGraphQLSchema(schema1, schema2) + + then: + diff.size() == 1 + + } + + def "inserting interface with same name as previous object"() { + given: + def schema1 = schema(""" + type Query { + luna: Pet + } + type Pet { + name: String + } + """) + def schema2 = schema(""" + type Query { + luna: Pet + } + interface Pet { + name: String + } + type Dog implements Pet { + name: String + } + """) + + when: + def diff = new SchemaDiffing().diffGraphQLSchema(schema1, schema2) + for (EditOperation operation : diff) { + println operation + } + + then: + /** + * If we would allow to map Object to Interface this would have a result of 8 + */ + diff.size() == 8 + + } + + def "remove scalars and add Enums"() { + given: + def schema1 = schema(""" + scalar S1 + scalar S2 + scalar S3 + enum E1{ + E1 + } + type Query { + s1: S1 + s2: S2 + s3: S3 + e1: E1 + } + """) + def schema2 = schema(""" + enum E1{ + E1 + } + enum E2{ + E2 + } + type Query { + e1: E1 + e2: E2 + } + """) + + when: + def diff = new SchemaDiffing().diffGraphQLSchema(schema1, schema2) + for (EditOperation operation : diff) { + println operation + } + + then: + diff.size() == 15 + + } + + def "change large schema a bit"() { + given: + def largeSchema = TestUtil.schemaFromResource("large-schema-2.graphqls", TestUtil.mockRuntimeWiring) + int counter = 0; + def changedOne = SchemaTransformer.transformSchema(largeSchema, new GraphQLTypeVisitorStub() { + @Override + TraversalControl visitGraphQLFieldDefinition(GraphQLFieldDefinition fieldDefinition, TraverserContext context) { + if (fieldDefinition.getName() == "field50") { + counter++; + return changeNode(context, fieldDefinition.transform({ it.name("field50Changed") })) + } + return TraversalControl.CONTINUE + } + }) + println "changed fields: " + counter + when: + def diff = new SchemaDiffing().diffGraphQLSchema(largeSchema, changedOne) + then: + diff.size() == 171 + } + + def "change large schema a bit 2"() { + given: + def largeSchema = TestUtil.schemaFromResource("large-schema-2.graphqls", TestUtil.mockRuntimeWiring) + int counter = 0; + def changedOne = SchemaTransformer.transformSchema(largeSchema, new GraphQLTypeVisitorStub() { + @Override + TraversalControl visitGraphQLFieldDefinition(GraphQLFieldDefinition fieldDefinition, TraverserContext context) { + if (fieldDefinition.getName() == "field50") { + counter++; + return deleteNode(context); + } + return TraversalControl.CONTINUE + } + }) + println "deleted fields: " + counter + when: + def diff = new SchemaDiffing().diffGraphQLSchema(largeSchema, changedOne) + diff.each { println it } + then: + // deleting 171 fields + dummyTypes + 3 edges for each field,dummyType pair = 5*171 + diff.size() == 3 * 171 + } + + def "change object type name used twice"() { + given: + def schema1 = schema(""" + type Query { + hello: Foo + hello2: Foo + } + type Foo { + foo: String + } + """) + def schema2 = schema(""" + type Query { + hello: Foo2 + hello2: Foo2 + } + type Foo2 { + foo: String + } + """) + + when: + def diff = new SchemaDiffing().diffGraphQLSchema(schema1, schema2) + + then: + diff.size() == 3 + + } + + def "change directive not applied"() { + given: + def schema1 = schema(""" + directive @foo on FIELD_DEFINITION + type Query { + hello: String + } + """) + def schema2 = schema(""" + directive @foo2 on FIELD_DEFINITION + type Query { + hello: String + } + """) + + when: + def diff = new SchemaDiffing().diffGraphQLSchema(schema1, schema2) + + then: + diff.size() == 1 + + } + + def "change directive which is also applied"() { + given: + def schema1 = schema(""" + directive @foo on FIELD_DEFINITION + type Query { + hello: String @foo + } + """) + def schema2 = schema(""" + directive @foo2 on FIELD_DEFINITION + type Query { + hello: String @foo2 + } + """) + + when: + def diff = new SchemaDiffing().diffGraphQLSchema(schema1, schema2) + + then: + diff.size() == 2 + + } + + def "delete a field"() { + given: + def schema1 = schema(""" + type Query { + hello: String + toDelete: String + } + """) + def schema2 = schema(""" + type Query { + hello: String + } + """) + + def diffing = new SchemaDiffing() + when: + def diff = diffing.diffGraphQLSchema(schema1, schema2) + for (EditOperation editOperation : diff) { + println editOperation + } + + then: + diff.size() == 3 + } + + + def "added different types and fields"() { + given: + def schema1 = schema(""" + type Query { + pets: [Pet] + } + interface Pet { + name: String + } + type Dog implements Pet { + name: String + } + type Cat implements Pet { + name: String + } + """) + def schema2 = schema(""" + type Query { + pets: [Animal] @deprecated + animals: [Animal] + } + interface Animal { + name: String + friend: Human + } + type Human { + name: String + } + interface Pet implements Animal { + name: String + friend: Human + } + type Dog implements Pet & Animal { + name: String + friend: Human + } + type Cat implements Pet & Animal { + name: String + friend: Human + } + type Fish implements Pet & Animal { + name: String + friend: Human + } + """) + + when: + def diff = new SchemaDiffing().diffGraphQLSchema(schema1, schema2) + diff.each { println it } + + then: + diff.size() == 41 + + } + + def "adding a few things "() { + given: + def schema1 = schema(""" + type Query { + pets: [Pet] + } + interface Pet { + name: String + } + type Dog implements Pet { + name: String + } + type Cat implements Pet { + name: String + } + """) + def schema2 = schema(""" + type Query { + pets: [Pet] + animals: [Animal] + } + interface Animal { + name: String + } + interface Pet { + name: String + } + type Dog implements Pet { + name: String + } + type Cat implements Pet { + name: String + } + type Fish implements Pet{ + name: String + } + """) + + when: + def operations = new SchemaDiffing().diffGraphQLSchema(schema1, schema2) + + then: + operations.size() == 12 + } + + def "adding a few things plus introducing new interface"() { + given: + def schema1 = schema(""" + type Query { + pets: [Pet] + } + interface Pet { + name: String + } + type Dog implements Pet { + name: String + } + type Cat implements Pet { + name: String + } + """) + def schema2 = schema(""" + type Query { + pets: [Pet] + animals: [Animal] + } + interface Animal { + name: String + } + interface Pet implements Animal { + name: String + } + type Dog implements Pet & Animal { + name: String + } + type Cat implements Pet & Animal { + name: String + } + type Fish implements Pet & Animal { + name: String + } + """) + + when: + def operations = new SchemaDiffing().diffGraphQLSchema(schema1, schema2) + + then: + operations.size() == 16 + } + + def "adding a few things plus introducing new interface plus changing return type"() { + given: + def schema1 = schema(""" + type Query { + pets: [Pet] + } + interface Pet { + name: String + } + type Dog implements Pet { + name: String + } + type Cat implements Pet { + name: String + } + """) + def schema2 = schema(""" + type Query { + pets: [Animal] + animals: [Animal] + } + interface Animal { + name: String + } + interface Pet implements Animal { + name: String + } + type Dog implements Pet & Animal { + name: String + } + type Cat implements Pet & Animal { + name: String + } + type Fish implements Pet & Animal { + name: String + } + """) + + when: + def operations = new SchemaDiffing().diffGraphQLSchema(schema1, schema2) + + then: + operations.size() == 18 + } + + def "adding a few things plus introducing new interface plus changing return type plus adding field in Interface"() { + given: + def schema1 = schema(""" + type Query { + pets: [Pet] + } + interface Pet { + name: String + } + type Dog implements Pet { + name: String + } + type Cat implements Pet { + name: String + } + """) + def schema2 = schema(""" + type Query { + pets: [Pet] + } + interface Animal { + name: String + friend: String + } + interface Pet implements Animal { + name: String + friend: String + } + type Dog implements Pet & Animal { + name: String + friend: String + } + type Cat implements Pet & Animal { + name: String + friend: String + } + type Fish implements Pet & Animal { + name: String + friend: String + } + """) + + when: + def operations = new SchemaDiffing().diffGraphQLSchema(schema1, schema2) + + then: + operations.size() == 28 + } + + def "add a field"() { + given: + def schema1 = schema(""" + type Query { + hello: String + } + """) + def schema2 = schema(""" + type Query { + hello: String + newField: String + } + """) + + when: + def diff = new SchemaDiffing().diffGraphQLSchema(schema1, schema2) + + then: + diff.size() == 3 + + } + + def "add a field and Type"() { + given: + def schema1 = schema(""" + type Query { + hello: String + } + """) + def schema2 = schema(""" + type Query { + hello: String + newField: Foo + } + type Foo { + foo: String + } + """) + + when: + def diff = new SchemaDiffing().diffGraphQLSchema(schema1, schema2) + diff.each { println it } + + then: + diff.size() == 7 + + } + + def "add a field and Type and remove a field"() { + given: + def schema1 = schema(""" + type Query { + hello: String + } + """) + def schema2 = schema(""" + type Query { + newField: Foo + } + type Foo { + foo: String + } + """) + + when: + def diff = new SchemaDiffing().diffGraphQLSchema(schema1, schema2) + + then: + diff.size() == 7 + + } + + + def "change a Union "() { + given: + def schema1 = schema(""" + type Query { + pet: Pet + } + union Pet = Dog | Cat + type Dog { + name: String + } + type Cat { + name: String + } + """) + def schema2 = schema(""" + type Query { + pet: Pet + } + union Pet = Dog | Bird | Fish + type Dog { + name: String + } + type Bird { + name: String + } + type Fish { + name: String + } + """) + + when: + def diff = new SchemaDiffing().diffGraphQLSchema(schema1, schema2) + + then: + /** + * 1. Change Cat to Bird + * 2,3: Insert Fish, Insert Fish.name + * 4. Insert Edge from Fish to Fish.name + * 5 Insert Edge from Fish.name -> String + * 6. Insert edge from Pet -> Fish + */ + diff.size() == 6 + + } + + def "adding an argument "() { + given: + def schema1 = schema(""" + type Query { + foo: String + } + """) + def schema2 = schema(""" + type Query { + foo(arg: Int): String + } + """) + + when: + def operations = new SchemaDiffing().diffGraphQLSchema(schema1, schema2) + + then: + operations.size() == 4 + } + + def "changing an argument "() { + given: + def schema1 = schema(""" + type Query { + foo(arg: Int): String + } + """) + def schema2 = schema(""" + type Query { + foo(arg2: Boolean): String + } + """) + + when: + def operations = new SchemaDiffing().diffGraphQLSchema(schema1, schema2) + + then: + operations.size() == 4 + } + + def "input fields"() { + given: + def schema1 = schema(""" + type Query { + foo(arg: I1, arg2: I2): String + } + input I1 { + f1: String + f2: String + } + input I2 { + g1: String + g2: String + } + """) + def schema2 = schema(""" + type Query { + foo(arg: I1,arg2: I2 ): String + } + input I1 { + f1: String + } + input I2 { + g2: String + g3: String + g4: String + } + """) + + when: + def operations = new SchemaDiffing().diffGraphQLSchema(schema1, schema2) + operations.each { println it } + + then: + /** + * The test here is that f2 is deleted and one g is renamed and g3 is inserted. + * It would be less operations with f2 renamed to g3, but this would defy expectations. + * + */ + operations.size() == 7 + } + + def "arguments in fields"() { + given: + def schema1 = schema(""" + type Query { + a(f1: String, f2:String): String + b(g1: String, g2:String): O1 + } + type O1 { + c(h1: String, h2:String): String + d(i1: String, i2:String): O1 + } + """) + def schema2 = schema(""" + type Query { + a(f1: String): String + b(g2: String, g3:String, g4: String): String + } + type O1 { + c(h1: String, h2:String): String + renamed(i2: String, i3:String): O1 + } + """) + + when: + def operations = new SchemaDiffing().diffGraphQLSchema(schema1, schema2) + operations.each { println it } + + then: + /** + * Query.f2 deleted + * O1.b.g1 => O1.b.g4 + * O1.d.i1 -> O.renamed.i3 + * O1.d => O1.renamed + * Inserted O1.b.g3 + */ + operations.size() == 11 + } + + def "same arguments in different contexts"() { + given: + def schema1 = schema(""" + type Query { + foo(someArg:String): String + } + """) + def schema2 = schema(""" + type Query { + field1(arg1: String): String + field2(arg1: String): String + field3(arg1: String): String + } + """) + + when: + def operations = new SchemaDiffing().diffGraphQLSchema(schema1, schema2) + operations.each { println it } + + then: + operations.size() == 14 + } + + def "adding enum value"() { + given: + def schema1 = schema(""" + type Query { + foo: Foo + } + enum Foo { + V1 + V2 + } + """) + def schema2 = schema(""" + type Query { + foo: Foo + } + enum Foo { + V1 + V2 + V3 + } + """) + + when: + def operations = new SchemaDiffing().diffGraphQLSchema(schema1, schema2) + + then: + operations.size() == 2 + } + + def "arguments in directives changed"() { + given: + def schema1 = schema(''' + directive @d(a1: String, a2: String) on FIELD_DEFINITION + type Query { + foo: String @d + } + ''') + def schema2 = schema(""" + directive @d(a1: String, a3: String, a4: String) on FIELD_DEFINITION + type Query { + foo: String @d + } + """) + + when: + def operations = new SchemaDiffing().diffGraphQLSchema(schema1, schema2) + operations.each { println it } + + then: + /** + * change: a2 => a3 + * insert: a4 + * new edge from directive to a4 + * new edge from a4 to String + */ + operations.size() == 4 + } + + def "change applied argument"() { + given: + def schema1 = schema(''' + directive @d(a1: String, a2: String) on FIELD_DEFINITION + type Query { + foo: String @d(a1: "S1", a2: "S2") + } + ''') + def schema2 = schema(""" + directive @d(a1: String, a2: String) on FIELD_DEFINITION + type Query { + foo: String @d(a2: "S2Changed", a1: "S1Changed") + } + """) + + when: + def operations = new SchemaDiffing().diffGraphQLSchema(schema1, schema2) + operations.each { println it } + + then: + operations.size() == 2 + } + + def "applied arguments in different contexts"() { + given: + def schema1 = schema(''' + directive @d(a1: String, a2: String, b1: String, b2: String, b3: String, b4: String) on FIELD_DEFINITION + type Query { + foo: String @d(a1: "a1", a2: "a2") + foo2: String @d(b1: "b1", b2: "b2") + } + ''') + def schema2 = schema(""" + directive @d(a1: String, a2: String, b1: String, b2: String, b3: String, b4: String) on FIELD_DEFINITION + type Query { + foo: String @d(a1: "a1") + foo2: String @d(b2: "b2", b3: "b3", b4: "b4") + } + """) + + when: + def operations = new SchemaDiffing().diffGraphQLSchema(schema1, schema2) + operations.each { println it } + + then: + /** + * The test here is that the context of the applied argument is considered and that a2 is deleted and one b is inserted and another one changed. + */ + operations.size() == 5 + } + + def "with directives"() { + given: + def schema1 = schema(''' + directive @TopLevelType on OBJECT + directive @specialId(type: String) on ARGUMENT_DEFINITION + type Query { + foo: Foo + } + type Foo @TopLevelType { + user(location: ID! @specialId(type : "someId"), limit: Int = 25, start: Int, title: String): PaginatedList + } + type PaginatedList { + count: Int + } + ''') + def schema2 = schema(""" + directive @TopLevelType on OBJECT + directive @specialId(type: String) on ARGUMENT_DEFINITION + type Query { + foo: Foo + } + type Foo @TopLevelType { + user(location: ID! @specialId(type : "someId"), limit: Int = 25, start: Int, title: String): PaginatedList + other(after: String, favourite: Boolean, first: Int = 25, location: ID! @specialId(type : "someId"), label: [String], offset: Int, status: String, type: String): PaginatedList + } + type PaginatedList { + count: Int + } + """) + + when: + def operations = new SchemaDiffing().diffGraphQLSchema(schema1, schema2) + + then: + operations.size() == 31 + } + + def "built in directives"() { + given: + def schema1 = schema(''' + directive @specialId(type: String) on FIELD_DEFINITION + + + type Query { + hello: String @specialId(type: "someId") + } + ''') + def schema2 = schema(""" + directive @specialId(type: String) on FIELD_DEFINITION + + type Query { + renamedHello: String @specialId(type: "otherId") + } + """) + + when: + def operations = new SchemaDiffing().diffGraphQLSchema(schema1, schema2) + operations.each { println it } + + then: + operations.size() == 2 + } + + def "unchanged scheme"() { + given: + def schema1 = schema(''' + directive @specialId(type: String) on FIELD_DEFINITION + directive @Magic(owner: String!, type: String!) on FIELD_DEFINITION | ARGUMENT_DEFINITION | INPUT_FIELD_DEFINITION + + + type Query { + hello: String @specialId(type: "someId") + foo(arg: Int, arg2: String = "hello"): [Foo]! + old: Boolean @deprecated + someOther(input1: MyInput, input2: OtherInput): E + } + type Foo { + id: ID + e1: E + union: MyUnion + } + union MyUnion = Foo | Bar + type Bar { + id: ID + } + enum E { + E1, E2, E3 + } + input MyInput { + id: ID + other: String! @Magic(owner: "Me", type: "SomeType") + } + input OtherInput { + inputField1: ID! @Magic(owner: "O1", type: "T1") + inputField2: ID! @Magic(owner: "O2", type: "T2") + } + ''') + + when: + def operations = new SchemaDiffing().diffGraphQLSchema(schema1, schema1) + operations.each { println it } + + then: + operations.size() == 0 + } + + def "changed query operation type "() { + given: + def schema1 = schema(''' + type Query { + foo: String + } + type MyQuery { + foo: String + } + ''') + def schema2 = schema(''' + schema { + query: MyQuery + } + type Query { + foo: String + } + type MyQuery { + foo: String + } + ''') + + when: + def operations = new SchemaDiffing().diffGraphQLSchema(schema1, schema2) + operations.each { println it } + + then: + // delete edge and insert new one + operations.size() == 2 + } + + def "applied schema directives"() { + given: + def schema1 = schema(''' + directive @foo(arg: String) on SCHEMA + + schema @foo(arg: "bar") { + query: MyQuery + } + type MyQuery { + foo: String + } + ''') + def schema2 = schema(''' + directive @foo(arg: String) on SCHEMA + + schema @foo(arg: "barChanged") { + query: MyQuery + } + type MyQuery { + foo: String + } + ''') + + when: + def operations = new SchemaDiffing().diffGraphQLSchema(schema1, schema2) + operations.each { println it } + + then: + // applied argument changed + operations.size() == 1 + + } + + def "change description"() { + given: + def schema1 = schema(''' + "Hello World" + type Query { + "helloDesc" + hello: String + } + ''') + def schema2 = schema(''' + "Hello World now" + type Query { + "helloDescChanged" + hello: String + } + ''') + + when: + def operations = new SchemaDiffing().diffGraphQLSchema(schema1, schema2) + operations.each { println it } + + then: + operations.size() == 2 + + } + + def "change default value"() { + given: + def schema1 = schema(''' + input I { + someNumber: Int = 100 + } + type Query { + hello(arg: String = "defaultValue", i: I): String + } + ''') + def schema2 = schema(''' + input I { + someNumber: Int = 200 + } + type Query { + hello(arg: String = "defaultValueChanged",i: I): String + } + ''') + + when: + def operations = new SchemaDiffing().diffGraphQLSchema(schema1, schema2) + operations.each { println it } + + then: + operations.size() == 2 + + } + + def "change field type, but not the wrapped type "() { + given: + def schema1 = schema(''' + type Query { + hello: String + hello2: String + } + ''') + def schema2 = schema(''' + type Query { + hello: String! + hello2: [[String!]] + } + ''') + + when: + def operations = new SchemaDiffing().diffGraphQLSchema(schema1, schema2) + operations.each { println it } + + then: + operations.size() == 2 + operations.findAll({ it.operation == EditOperation.Operation.CHANGE_EDGE }).size() == 2 + + } + + def "Recursive input field with default "() { + given: + def schema1 = schema(''' + input I { + name: String + field: I = {name: "default name"} + } + type Query { + foo(arg: I): String + } + ''') + def schema2 = schema(''' + input I { + name: String + field: [I] = [{name: "default name"}] + } + type Query { + foo(arg: I): String + } + ''') + + when: + def operations = new SchemaDiffing().diffGraphQLSchema(schema1, schema2) + operations.each { println it } + + then: + // changing the label of the edge to the type + operations.size() == 1 + operations.findAll({ it.operation == EditOperation.Operation.CHANGE_EDGE }).size() == 1 + + } + + def "directive argument default value changed"() { + given: + def schema1 = schema(''' + type Query { + foo: String + } + directive @d(foo:String = "A") on FIELD + ''') + def schema2 = schema(''' + type Query { + foo: String + } + directive @d(foo: String = "B") on FIELD + ''') + + when: + def operations = new SchemaDiffing().diffGraphQLSchema(schema1, schema2) + operations.each { println it } + + then: + // changing the label of the edge to the type + operations.size() == 1 + operations.findAll({ it.operation == EditOperation.Operation.CHANGE_EDGE }).size() == 1 + + + } + + def "object applied directive argument change"() { + given: + def schema1 = schema(''' + directive @d(arg:String) on FIELD_DEFINITION + + type Query { + foo: String @d(arg: "foo") + } + + ''') + def schema2 = schema(''' + directive @d(arg: String) on FIELD_DEFINITION + + type Query { + foo: String @d(arg: "bar") + } + ''') + + when: + def operations = new SchemaDiffing().diffGraphQLSchema(schema1, schema2) + operations.each { println it } + + then: + operations.size() == 1 + } + + def "object applied directive rename"() { + given: + def schema1 = schema(''' + directive @d1(arg:String) on FIELD_DEFINITION + directive @d2(arg:String) on FIELD_DEFINITION + + type Query { + foo: String @d1(arg: "foo") + } + + ''') + def schema2 = schema(''' + directive @d1(arg:String) on FIELD_DEFINITION + directive @d2(arg:String) on FIELD_DEFINITION + + type Query { + foo: String @d2(arg: "foo") + } + ''') + + when: + def operations = new SchemaDiffing().diffGraphQLSchema(schema1, schema2) + operations.each { println it } + + then: + operations.size() == 1 + } + +} + + diff --git a/src/test/groovy/graphql/schema/diffing/ana/EditOperationAnalyzerAppliedDirectivesTest.groovy b/src/test/groovy/graphql/schema/diffing/ana/EditOperationAnalyzerAppliedDirectivesTest.groovy new file mode 100644 index 0000000000..4e1c30a21d --- /dev/null +++ b/src/test/groovy/graphql/schema/diffing/ana/EditOperationAnalyzerAppliedDirectivesTest.groovy @@ -0,0 +1,822 @@ +package graphql.schema.diffing.ana + +import graphql.TestUtil +import graphql.schema.diffing.SchemaDiffing +import spock.lang.Specification + +import static graphql.schema.diffing.ana.SchemaDifference.AppliedDirectiveAddition +import static graphql.schema.diffing.ana.SchemaDifference.AppliedDirectiveArgumentDeletion +import static graphql.schema.diffing.ana.SchemaDifference.AppliedDirectiveArgumentRename +import static graphql.schema.diffing.ana.SchemaDifference.AppliedDirectiveArgumentValueModification +import static graphql.schema.diffing.ana.SchemaDifference.AppliedDirectiveDeletion +import static graphql.schema.diffing.ana.SchemaDifference.AppliedDirectiveDirectiveArgumentLocation +import static graphql.schema.diffing.ana.SchemaDifference.AppliedDirectiveEnumLocation +import static graphql.schema.diffing.ana.SchemaDifference.AppliedDirectiveEnumValueLocation +import static graphql.schema.diffing.ana.SchemaDifference.AppliedDirectiveInputObjectFieldLocation +import static graphql.schema.diffing.ana.SchemaDifference.AppliedDirectiveInputObjectLocation +import static graphql.schema.diffing.ana.SchemaDifference.AppliedDirectiveInterfaceFieldArgumentLocation +import static graphql.schema.diffing.ana.SchemaDifference.AppliedDirectiveInterfaceFieldLocation +import static graphql.schema.diffing.ana.SchemaDifference.AppliedDirectiveInterfaceLocation +import static graphql.schema.diffing.ana.SchemaDifference.AppliedDirectiveObjectFieldArgumentLocation +import static graphql.schema.diffing.ana.SchemaDifference.AppliedDirectiveObjectFieldLocation +import static graphql.schema.diffing.ana.SchemaDifference.AppliedDirectiveObjectLocation +import static graphql.schema.diffing.ana.SchemaDifference.AppliedDirectiveScalarLocation +import static graphql.schema.diffing.ana.SchemaDifference.AppliedDirectiveUnionLocation +import static graphql.schema.diffing.ana.SchemaDifference.DirectiveModification +import static graphql.schema.diffing.ana.SchemaDifference.EnumModification +import static graphql.schema.diffing.ana.SchemaDifference.InputObjectModification +import static graphql.schema.diffing.ana.SchemaDifference.InterfaceModification +import static graphql.schema.diffing.ana.SchemaDifference.ObjectModification +import static graphql.schema.diffing.ana.SchemaDifference.ScalarModification +import static graphql.schema.diffing.ana.SchemaDifference.UnionModification + +class EditOperationAnalyzerAppliedDirectivesTest extends Specification { + + def "applied directive argument deleted interface field "() { + given: + def oldSdl = ''' + directive @d(arg1:String) on FIELD_DEFINITION + + type Query implements I{ + foo: String + } + interface I { + foo: String @d(arg1: "foo") + } + ''' + def newSdl = ''' + directive @d(arg1:String) on FIELD_DEFINITION + + type Query implements I{ + foo: String + } + interface I { + foo: String @d + } + ''' + when: + def changes = calcDiff(oldSdl, newSdl) + then: + changes.interfaceDifferences["I"] instanceof InterfaceModification + def argumentDeletions = (changes.interfaceDifferences["I"] as InterfaceModification).getDetails(AppliedDirectiveArgumentDeletion) + def location = argumentDeletions[0].locationDetail as AppliedDirectiveInterfaceFieldLocation + location.interfaceName == "I" + location.fieldName == "foo" + argumentDeletions[0].argumentName == "arg1" + } + + def "applied directive added input object field "() { + given: + def oldSdl = ''' + directive @d(arg:String) on INPUT_FIELD_DEFINITION + input I { + a: String + } + type Query { + foo(arg: I): String + } + ''' + def newSdl = ''' + directive @d(arg:String) on INPUT_FIELD_DEFINITION + input I { + a: String @d(arg: "foo") + } + type Query { + foo(arg: I): String + } + ''' + when: + def changes = calcDiff(oldSdl, newSdl) + then: + changes.inputObjectDifferences["I"] instanceof InputObjectModification + def appliedDirective = (changes.inputObjectDifferences["I"] as InputObjectModification).getDetails(AppliedDirectiveAddition) + (appliedDirective[0].locationDetail as AppliedDirectiveInputObjectFieldLocation).inputObjectName == "I" + (appliedDirective[0].locationDetail as AppliedDirectiveInputObjectFieldLocation).fieldName == "a" + appliedDirective[0].name == "d" + } + + def "applied directive added object field"() { + given: + def oldSdl = ''' + directive @d(arg:String) on FIELD_DEFINITION + + type Query { + foo: String + } + ''' + def newSdl = ''' + directive @d(arg: String) on FIELD_DEFINITION + + type Query { + foo: String @d(arg: "foo") + } + ''' + when: + def changes = calcDiff(oldSdl, newSdl) + then: + changes.objectDifferences["Query"] instanceof ObjectModification + def appliedDirective = (changes.objectDifferences["Query"] as ObjectModification).getDetails(AppliedDirectiveAddition) + (appliedDirective[0].locationDetail as AppliedDirectiveObjectFieldLocation).objectName == "Query" + (appliedDirective[0].locationDetail as AppliedDirectiveObjectFieldLocation).fieldName == "foo" + appliedDirective[0].name == "d" + } + + def "applied directive argument value changed object field "() { + given: + def oldSdl = ''' + directive @d(arg:String) on FIELD_DEFINITION + + type Query { + foo: String @d(arg: "foo1") + } + ''' + def newSdl = ''' + directive @d(arg: String) on FIELD_DEFINITION + + type Query { + foo: String @d(arg: "foo2") + } + ''' + when: + def changes = calcDiff(oldSdl, newSdl) + then: + changes.objectDifferences["Query"] instanceof ObjectModification + def argumentValueModifications = (changes.objectDifferences["Query"] as ObjectModification).getDetails(AppliedDirectiveArgumentValueModification) + (argumentValueModifications[0].locationDetail as AppliedDirectiveObjectFieldLocation).objectName == "Query" + (argumentValueModifications[0].locationDetail as AppliedDirectiveObjectFieldLocation).fieldName == "foo" + argumentValueModifications[0].argumentName == "arg" + argumentValueModifications[0].oldValue == '"foo1"' + argumentValueModifications[0].newValue == '"foo2"' + } + + def "applied directive argument name changed object field"() { + given: + def oldSdl = ''' + directive @d(arg1:String, arg2: String) on FIELD_DEFINITION + + type Query { + foo: String @d(arg1: "foo") + } + ''' + def newSdl = ''' + directive @d(arg1: String, arg2: String) on FIELD_DEFINITION + + type Query { + foo: String @d(arg2: "foo") + } + ''' + when: + def changes = calcDiff(oldSdl, newSdl) + then: + changes.objectDifferences["Query"] instanceof ObjectModification + def argumentRenames = (changes.objectDifferences["Query"] as ObjectModification).getDetails(AppliedDirectiveArgumentRename) + def location = argumentRenames[0].locationDetail as AppliedDirectiveObjectFieldLocation + location.objectName == "Query" + location.fieldName == "foo" + argumentRenames[0].oldName == "arg1" + argumentRenames[0].newName == "arg2" + } + + def "applied directive argument deleted object field"() { + given: + def oldSdl = ''' + directive @d(arg1:String) on FIELD_DEFINITION + + type Query { + foo: String @d(arg1: "foo") + } + ''' + def newSdl = ''' + directive @d(arg1: String) on FIELD_DEFINITION + + type Query { + foo: String @d + } + ''' + when: + def changes = calcDiff(oldSdl, newSdl) + then: + changes.objectDifferences["Query"] instanceof ObjectModification + def argumentDeletions = (changes.objectDifferences["Query"] as ObjectModification).getDetails(AppliedDirectiveArgumentDeletion) + def location = argumentDeletions[0].locationDetail as AppliedDirectiveObjectFieldLocation + location.objectName == "Query" + location.fieldName == "foo" + argumentDeletions[0].argumentName == "arg1" + } + + def "applied directive added input object"() { + given: + def oldSdl = ''' + directive @d(arg:String) on INPUT_OBJECT + input I { + a: String + } + type Query { + foo(arg: I): String + } + ''' + def newSdl = ''' + directive @d(arg:String) on INPUT_OBJECT + input I @d(arg: "foo") { + a: String + } + type Query { + foo(arg: I): String + } + ''' + when: + def changes = calcDiff(oldSdl, newSdl) + then: + changes.inputObjectDifferences["I"] instanceof InputObjectModification + def appliedDirective = (changes.inputObjectDifferences["I"] as InputObjectModification).getDetails(AppliedDirectiveAddition) + (appliedDirective[0].locationDetail as AppliedDirectiveInputObjectLocation).name == "I" + appliedDirective[0].name == "d" + } + + def "applied directive added object"() { + given: + def oldSdl = ''' + directive @d(arg:String) on OBJECT + + type Query { + foo: String + } + ''' + def newSdl = ''' + directive @d(arg: String) on OBJECT + + type Query @d(arg: "foo") { + foo: String + } + ''' + when: + def changes = calcDiff(oldSdl, newSdl) + then: + changes.objectDifferences["Query"] instanceof ObjectModification + def appliedDirective = (changes.objectDifferences["Query"] as ObjectModification).getDetails(AppliedDirectiveAddition) + (appliedDirective[0].locationDetail as AppliedDirectiveObjectLocation).name == "Query" + appliedDirective[0].name == "d" + } + + def "applied directive added interface"() { + given: + def oldSdl = ''' + directive @d(arg:String) on INTERFACE + + type Query implements I{ + foo: String + } + interface I { + foo: String + } + ''' + def newSdl = ''' + directive @d(arg: String) on INTERFACE + + type Query implements I { + foo: String + } + interface I @d(arg: "foo") { + foo: String + } + ''' + when: + def changes = calcDiff(oldSdl, newSdl) + then: + changes.interfaceDifferences["I"] instanceof InterfaceModification + def appliedDirective = (changes.interfaceDifferences["I"] as InterfaceModification).getDetails(AppliedDirectiveAddition) + (appliedDirective[0].locationDetail as AppliedDirectiveInterfaceLocation).name == "I" + appliedDirective[0].name == "d" + } + + def "applied directive added union"() { + given: + def oldSdl = ''' + directive @d(arg: String) on UNION + type Query { + foo: U + } + union U = A | B + type A { a: String } + type B { b: String } + ''' + def newSdl = ''' + directive @d(arg: String) on UNION + type Query { + foo: U + } + union U @d(arg: "foo") = A | B + type A { a: String } + type B { b: String } + ''' + when: + def changes = calcDiff(oldSdl, newSdl) + then: + changes.unionDifferences["U"] instanceof UnionModification + def appliedDirective = (changes.unionDifferences["U"] as UnionModification).getDetails(AppliedDirectiveAddition) + (appliedDirective[0].locationDetail as AppliedDirectiveUnionLocation).name == "U" + appliedDirective[0].name == "d" + } + + def "applied directive added scalar"() { + given: + def oldSdl = ''' + directive @d(arg:String) on SCALAR + scalar DateTime + type Query { + foo: DateTime + } + ''' + def newSdl = ''' + directive @d(arg:String) on SCALAR + scalar DateTime @d(arg: "foo") + type Query { + foo: DateTime + } + ''' + when: + def changes = calcDiff(oldSdl, newSdl) + then: + changes.scalarDifferences["DateTime"] instanceof ScalarModification + def appliedDirective = (changes.scalarDifferences["DateTime"] as ScalarModification).getDetails(AppliedDirectiveAddition) + (appliedDirective[0].locationDetail as AppliedDirectiveScalarLocation).name == "DateTime" + appliedDirective[0].name == "d" + } + + def "applied directive added enum"() { + given: + def oldSdl = ''' + directive @d(arg:String) on ENUM + enum E { A, B } + type Query { + foo: E + } + ''' + def newSdl = ''' + directive @d(arg:String) on ENUM + enum E @d(arg: "foo") { A, B } + type Query { + foo: E + } + ''' + when: + def changes = calcDiff(oldSdl, newSdl) + then: + changes.enumDifferences["E"] instanceof EnumModification + def appliedDirective = (changes.enumDifferences["E"] as EnumModification).getDetails(AppliedDirectiveAddition) + (appliedDirective[0].locationDetail as AppliedDirectiveEnumLocation).name == "E" + appliedDirective[0].name == "d" + } + + def "applied directive added enum value"() { + given: + def oldSdl = ''' + directive @d(arg:String) on ENUM_VALUE + enum E { A, B } + type Query { + foo: E + } + ''' + def newSdl = ''' + directive @d(arg:String) on ENUM_VALUE + enum E { A, B @d(arg: "foo") } + type Query { + foo: E + } + ''' + when: + def changes = calcDiff(oldSdl, newSdl) + then: + changes.enumDifferences["E"] instanceof EnumModification + def appliedDirective = (changes.enumDifferences["E"] as EnumModification).getDetails(AppliedDirectiveAddition) + (appliedDirective[0].locationDetail as AppliedDirectiveEnumValueLocation).enumName == "E" + (appliedDirective[0].locationDetail as AppliedDirectiveEnumValueLocation).valueName == "B" + appliedDirective[0].name == "d" + } + + def "applied directive added object field argument"() { + given: + def oldSdl = ''' + directive @d(arg:String) on ARGUMENT_DEFINITION + type Query { + foo(arg: String) : String + } + ''' + def newSdl = ''' + directive @d(arg:String) on ARGUMENT_DEFINITION + type Query { + foo(arg: String @d(arg: "foo")) : String + } + ''' + when: + def changes = calcDiff(oldSdl, newSdl) + then: + changes.objectDifferences["Query"] instanceof ObjectModification + def appliedDirective = (changes.objectDifferences["Query"] as ObjectModification).getDetails(AppliedDirectiveAddition) + (appliedDirective[0].locationDetail as AppliedDirectiveObjectFieldArgumentLocation).objectName == "Query" + (appliedDirective[0].locationDetail as AppliedDirectiveObjectFieldArgumentLocation).fieldName == "foo" + (appliedDirective[0].locationDetail as AppliedDirectiveObjectFieldArgumentLocation).argumentName == "arg" + appliedDirective[0].name == "d" + } + + def "applied directive added interface field argument"() { + given: + def oldSdl = ''' + directive @d(arg:String) on ARGUMENT_DEFINITION + type Query implements I { + foo(arg: String) : String + } + interface I { + foo(arg: String): String + } + ''' + def newSdl = ''' + directive @d(arg:String) on ARGUMENT_DEFINITION + type Query implements I { + foo(arg: String) : String + } + interface I { + foo(arg: String @d): String + } + ''' + when: + def changes = calcDiff(oldSdl, newSdl) + then: + changes.interfaceDifferences["I"] instanceof InterfaceModification + def appliedDirective = (changes.interfaceDifferences["I"] as InterfaceModification).getDetails(AppliedDirectiveAddition) + (appliedDirective[0].locationDetail as AppliedDirectiveInterfaceFieldArgumentLocation).interfaceName == "I" + (appliedDirective[0].locationDetail as AppliedDirectiveInterfaceFieldArgumentLocation).fieldName == "foo" + (appliedDirective[0].locationDetail as AppliedDirectiveInterfaceFieldArgumentLocation).argumentName == "arg" + appliedDirective[0].name == "d" + } + + def "applied directive added directive argument "() { + given: + def oldSdl = ''' + directive @d(arg:String) on ARGUMENT_DEFINITION + directive @d2(arg:String) on ARGUMENT_DEFINITION + type Query { + foo: String + } + ''' + def newSdl = ''' + directive @d(arg:String) on ARGUMENT_DEFINITION + directive @d2(arg:String @d) on ARGUMENT_DEFINITION + type Query { + foo: String + } + ''' + when: + def changes = calcDiff(oldSdl, newSdl) + then: + changes.directiveDifferences["d2"] instanceof DirectiveModification + def appliedDirective = (changes.directiveDifferences["d2"] as DirectiveModification).getDetails(AppliedDirectiveAddition) + (appliedDirective[0].locationDetail as AppliedDirectiveDirectiveArgumentLocation).directiveName == "d2" + (appliedDirective[0].locationDetail as AppliedDirectiveDirectiveArgumentLocation).argumentName == "arg" + appliedDirective[0].name == "d" + } + + def "applied directive deleted object"() { + given: + def oldSdl = ''' + directive @d(arg: String) on OBJECT + + type Query @d(arg: "foo") { + foo: String + } + ''' + def newSdl = ''' + directive @d(arg:String) on OBJECT + + type Query { + foo: String + } + ''' + when: + def changes = calcDiff(oldSdl, newSdl) + then: + changes.objectDifferences["Query"] instanceof ObjectModification + def appliedDirective = (changes.objectDifferences["Query"] as ObjectModification).getDetails(AppliedDirectiveDeletion) + (appliedDirective[0].locationDetail as AppliedDirectiveObjectLocation).name == "Query" + appliedDirective[0].name == "d" + } + + def "applied directive deleted directive argument "() { + given: + def oldSdl = ''' + directive @d(arg:String) on ARGUMENT_DEFINITION + directive @d2(arg:String @d) on ARGUMENT_DEFINITION + type Query { + foo: String + } + ''' + def newSdl = ''' + directive @d(arg:String) on ARGUMENT_DEFINITION + directive @d2(arg:String) on ARGUMENT_DEFINITION + type Query { + foo: String + } + ''' + when: + def changes = calcDiff(oldSdl, newSdl) + then: + changes.directiveDifferences["d2"] instanceof DirectiveModification + def appliedDirective = (changes.directiveDifferences["d2"] as DirectiveModification).getDetails(AppliedDirectiveDeletion) + (appliedDirective[0].locationDetail as AppliedDirectiveDirectiveArgumentLocation).directiveName == "d2" + (appliedDirective[0].locationDetail as AppliedDirectiveDirectiveArgumentLocation).argumentName == "arg" + appliedDirective[0].name == "d" + } + + def "applied directive deleted enum"() { + given: + def oldSdl = ''' + directive @d(arg:String) on ENUM + enum E @d(arg: "foo") { A, B } + type Query { + foo: E + } + ''' + def newSdl = ''' + directive @d(arg:String) on ENUM + enum E { A, B } + type Query { + foo: E + } + ''' + when: + def changes = calcDiff(oldSdl, newSdl) + then: + changes.enumDifferences["E"] instanceof EnumModification + def appliedDirective = (changes.enumDifferences["E"] as EnumModification).getDetails(AppliedDirectiveDeletion) + (appliedDirective[0].locationDetail as AppliedDirectiveEnumLocation).name == "E" + appliedDirective[0].name == "d" + } + + def "applied directive deleted enum value"() { + given: + def oldSdl = ''' + directive @d(arg:String) on ENUM_VALUE + enum E { A, B @d(arg: "foo") } + type Query { + foo: E + } + ''' + def newSdl = ''' + directive @d(arg:String) on ENUM_VALUE + enum E { A, B } + type Query { + foo: E + } + ''' + when: + def changes = calcDiff(oldSdl, newSdl) + then: + changes.enumDifferences["E"] instanceof EnumModification + def appliedDirective = (changes.enumDifferences["E"] as EnumModification).getDetails(AppliedDirectiveDeletion) + (appliedDirective[0].locationDetail as AppliedDirectiveEnumValueLocation).enumName == "E" + (appliedDirective[0].locationDetail as AppliedDirectiveEnumValueLocation).valueName == "B" + appliedDirective[0].name == "d" + } + + + def "applied directive deleted input object"() { + given: + def oldSdl = ''' + directive @d(arg:String) on INPUT_OBJECT + input I @d(arg: "foo") { + a: String + } + type Query { + foo(arg: I): String + } + ''' + def newSdl = ''' + directive @d(arg:String) on INPUT_OBJECT + input I { + a: String + } + type Query { + foo(arg: I): String + } + ''' + when: + def changes = calcDiff(oldSdl, newSdl) + then: + changes.inputObjectDifferences["I"] instanceof InputObjectModification + def appliedDirective = (changes.inputObjectDifferences["I"] as InputObjectModification).getDetails(AppliedDirectiveDeletion) + (appliedDirective[0].locationDetail as AppliedDirectiveInputObjectLocation).name == "I" + appliedDirective[0].name == "d" + } + + def "applied directive deleted input object field "() { + given: + def oldSdl = ''' + directive @d(arg:String) on INPUT_FIELD_DEFINITION + input I { + a: String @d(arg: "foo") + } + type Query { + foo(arg: I): String + } + ''' + def newSdl = ''' + directive @d(arg:String) on INPUT_FIELD_DEFINITION + input I { + a: String + } + type Query { + foo(arg: I): String + } + ''' + when: + def changes = calcDiff(oldSdl, newSdl) + then: + changes.inputObjectDifferences["I"] instanceof InputObjectModification + def appliedDirective = (changes.inputObjectDifferences["I"] as InputObjectModification).getDetails(AppliedDirectiveDeletion) + (appliedDirective[0].locationDetail as AppliedDirectiveInputObjectFieldLocation).inputObjectName == "I" + (appliedDirective[0].locationDetail as AppliedDirectiveInputObjectFieldLocation).fieldName == "a" + appliedDirective[0].name == "d" + } + + def "applied directive deleted interface"() { + given: + def oldSdl = ''' + directive @d(arg: String) on INTERFACE + + type Query implements I { + foo: String + } + interface I @d(arg: "foo") { + foo: String + } + ''' + def newSdl = ''' + directive @d(arg:String) on INTERFACE + + type Query implements I{ + foo: String + } + interface I { + foo: String + } + ''' + when: + def changes = calcDiff(oldSdl, newSdl) + then: + changes.interfaceDifferences["I"] instanceof InterfaceModification + def appliedDirective = (changes.interfaceDifferences["I"] as InterfaceModification).getDetails(AppliedDirectiveDeletion) + (appliedDirective[0].locationDetail as AppliedDirectiveInterfaceLocation).name == "I" + appliedDirective[0].name == "d" + } + + + def "applied directive deleted interface field argument"() { + given: + def oldSdl = ''' + directive @d(arg:String) on ARGUMENT_DEFINITION + type Query implements I { + foo(arg: String) : String + } + interface I { + foo(arg: String @d): String + } + ''' + def newSdl = ''' + directive @d(arg:String) on ARGUMENT_DEFINITION + type Query implements I { + foo(arg: String) : String + } + interface I { + foo(arg: String): String + } + ''' + when: + def changes = calcDiff(oldSdl, newSdl) + then: + changes.interfaceDifferences["I"] instanceof InterfaceModification + def appliedDirective = (changes.interfaceDifferences["I"] as InterfaceModification).getDetails(AppliedDirectiveDeletion) + (appliedDirective[0].locationDetail as AppliedDirectiveInterfaceFieldArgumentLocation).interfaceName == "I" + (appliedDirective[0].locationDetail as AppliedDirectiveInterfaceFieldArgumentLocation).fieldName == "foo" + (appliedDirective[0].locationDetail as AppliedDirectiveInterfaceFieldArgumentLocation).argumentName == "arg" + appliedDirective[0].name == "d" + } + + def "applied directive deleted object field"() { + given: + def oldSdl = ''' + directive @d(arg: String) on FIELD_DEFINITION + + type Query { + foo: String @d(arg: "foo") + } + ''' + def newSdl = ''' + directive @d(arg:String) on FIELD_DEFINITION + + type Query { + foo: String + } + ''' + when: + def changes = calcDiff(oldSdl, newSdl) + then: + changes.objectDifferences["Query"] instanceof ObjectModification + def appliedDirective = (changes.objectDifferences["Query"] as ObjectModification).getDetails(AppliedDirectiveDeletion) + (appliedDirective[0].locationDetail as AppliedDirectiveObjectFieldLocation).objectName == "Query" + (appliedDirective[0].locationDetail as AppliedDirectiveObjectFieldLocation).fieldName == "foo" + appliedDirective[0].name == "d" + } + + def "applied directive deleted object field argument"() { + given: + def oldSdl = ''' + directive @d(arg:String) on ARGUMENT_DEFINITION + type Query { + foo(arg: String @d(arg: "foo")) : String + } + ''' + def newSdl = ''' + directive @d(arg:String) on ARGUMENT_DEFINITION + type Query { + foo(arg: String) : String + } + ''' + when: + def changes = calcDiff(oldSdl, newSdl) + then: + changes.objectDifferences["Query"] instanceof ObjectModification + def appliedDirective = (changes.objectDifferences["Query"] as ObjectModification).getDetails(AppliedDirectiveDeletion) + (appliedDirective[0].locationDetail as AppliedDirectiveObjectFieldArgumentLocation).objectName == "Query" + (appliedDirective[0].locationDetail as AppliedDirectiveObjectFieldArgumentLocation).fieldName == "foo" + (appliedDirective[0].locationDetail as AppliedDirectiveObjectFieldArgumentLocation).argumentName == "arg" + appliedDirective[0].name == "d" + } + + + def "applied directive deleted scalar"() { + given: + def oldSdl = ''' + directive @d(arg:String) on SCALAR + scalar DateTime @d(arg: "foo") + type Query { + foo: DateTime + } + ''' + def newSdl = ''' + directive @d(arg:String) on SCALAR + scalar DateTime + type Query { + foo: DateTime + } + ''' + when: + def changes = calcDiff(oldSdl, newSdl) + then: + changes.scalarDifferences["DateTime"] instanceof ScalarModification + def appliedDirective = (changes.scalarDifferences["DateTime"] as ScalarModification).getDetails(AppliedDirectiveDeletion) + (appliedDirective[0].locationDetail as AppliedDirectiveScalarLocation).name == "DateTime" + appliedDirective[0].name == "d" + } + + + def "applied directive deleted union"() { + given: + def oldSdl = ''' + directive @d(arg: String) on UNION + type Query { + foo: U + } + union U @d(arg: "foo") = A | B + type A { a: String } + type B { b: String } + ''' + def newSdl = ''' + directive @d(arg: String) on UNION + type Query { + foo: U + } + union U = A | B + type A { a: String } + type B { b: String } + ''' + when: + def changes = calcDiff(oldSdl, newSdl) + then: + changes.unionDifferences["U"] instanceof UnionModification + def appliedDirective = (changes.unionDifferences["U"] as UnionModification).getDetails(AppliedDirectiveDeletion) + (appliedDirective[0].locationDetail as AppliedDirectiveUnionLocation).name == "U" + appliedDirective[0].name == "d" + } + + + EditOperationAnalysisResult calcDiff( + String oldSdl, + String newSdl + ) { + def oldSchema = TestUtil.schema(oldSdl) + def newSchema = TestUtil.schema(newSdl) + def changes = new SchemaDiffing().diffAndAnalyze(oldSchema, newSchema) + return changes + } + +} diff --git a/src/test/groovy/graphql/schema/diffing/ana/EditOperationAnalyzerTest.groovy b/src/test/groovy/graphql/schema/diffing/ana/EditOperationAnalyzerTest.groovy new file mode 100644 index 0000000000..a60a747a3d --- /dev/null +++ b/src/test/groovy/graphql/schema/diffing/ana/EditOperationAnalyzerTest.groovy @@ -0,0 +1,1671 @@ +package graphql.schema.diffing.ana + +import com.sun.xml.internal.bind.v2.schemagen.xmlschema.Union +import graphql.TestUtil +import graphql.schema.diffing.SchemaDiffing +import spock.lang.Specification + +import static graphql.schema.diffing.ana.SchemaDifference.* +import static graphql.schema.diffing.ana.SchemaDifference.AppliedDirectiveAddition +import static graphql.schema.diffing.ana.SchemaDifference.AppliedDirectiveArgumentValueModification +import static graphql.schema.diffing.ana.SchemaDifference.AppliedDirectiveObjectFieldLocation +import static graphql.schema.diffing.ana.SchemaDifference.DirectiveAddition +import static graphql.schema.diffing.ana.SchemaDifference.DirectiveArgumentAddition +import static graphql.schema.diffing.ana.SchemaDifference.DirectiveArgumentDefaultValueModification +import static graphql.schema.diffing.ana.SchemaDifference.DirectiveArgumentDeletion +import static graphql.schema.diffing.ana.SchemaDifference.DirectiveArgumentRename +import static graphql.schema.diffing.ana.SchemaDifference.DirectiveArgumentTypeModification +import static graphql.schema.diffing.ana.SchemaDifference.DirectiveDeletion +import static graphql.schema.diffing.ana.SchemaDifference.DirectiveModification +import static graphql.schema.diffing.ana.SchemaDifference.EnumAddition +import static graphql.schema.diffing.ana.SchemaDifference.EnumDeletion +import static graphql.schema.diffing.ana.SchemaDifference.EnumModification +import static graphql.schema.diffing.ana.SchemaDifference.EnumValueAddition +import static graphql.schema.diffing.ana.SchemaDifference.EnumValueDeletion +import static graphql.schema.diffing.ana.SchemaDifference.InputObjectAddition +import static graphql.schema.diffing.ana.SchemaDifference.InputObjectDeletion +import static graphql.schema.diffing.ana.SchemaDifference.InputObjectModification +import static graphql.schema.diffing.ana.SchemaDifference.InterfaceAddition +import static graphql.schema.diffing.ana.SchemaDifference.InterfaceFieldAddition +import static graphql.schema.diffing.ana.SchemaDifference.InterfaceFieldArgumentAddition +import static graphql.schema.diffing.ana.SchemaDifference.InterfaceFieldArgumentDefaultValueModification +import static graphql.schema.diffing.ana.SchemaDifference.InterfaceFieldArgumentDeletion +import static graphql.schema.diffing.ana.SchemaDifference.InterfaceFieldArgumentRename +import static graphql.schema.diffing.ana.SchemaDifference.InterfaceFieldArgumentTypeModification +import static graphql.schema.diffing.ana.SchemaDifference.InterfaceFieldDeletion +import static graphql.schema.diffing.ana.SchemaDifference.InterfaceFieldRename +import static graphql.schema.diffing.ana.SchemaDifference.InterfaceFieldTypeModification +import static graphql.schema.diffing.ana.SchemaDifference.InterfaceInterfaceImplementationDeletion +import static graphql.schema.diffing.ana.SchemaDifference.InterfaceModification +import static graphql.schema.diffing.ana.SchemaDifference.ObjectAddition +import static graphql.schema.diffing.ana.SchemaDifference.ObjectDeletion +import static graphql.schema.diffing.ana.SchemaDifference.ObjectFieldAddition +import static graphql.schema.diffing.ana.SchemaDifference.ObjectFieldArgumentAddition +import static graphql.schema.diffing.ana.SchemaDifference.ObjectFieldArgumentDefaultValueModification +import static graphql.schema.diffing.ana.SchemaDifference.ObjectFieldArgumentDeletion +import static graphql.schema.diffing.ana.SchemaDifference.ObjectFieldArgumentRename +import static graphql.schema.diffing.ana.SchemaDifference.ObjectFieldArgumentTypeModification +import static graphql.schema.diffing.ana.SchemaDifference.ObjectFieldDeletion +import static graphql.schema.diffing.ana.SchemaDifference.ObjectFieldRename +import static graphql.schema.diffing.ana.SchemaDifference.ObjectFieldTypeModification +import static graphql.schema.diffing.ana.SchemaDifference.ObjectInterfaceImplementationAddition +import static graphql.schema.diffing.ana.SchemaDifference.ObjectInterfaceImplementationDeletion +import static graphql.schema.diffing.ana.SchemaDifference.ObjectModification +import static graphql.schema.diffing.ana.SchemaDifference.ScalarAddition +import static graphql.schema.diffing.ana.SchemaDifference.ScalarDeletion +import static graphql.schema.diffing.ana.SchemaDifference.ScalarModification +import static graphql.schema.diffing.ana.SchemaDifference.UnionAddition +import static graphql.schema.diffing.ana.SchemaDifference.UnionDeletion +import static graphql.schema.diffing.ana.SchemaDifference.UnionMemberAddition +import static graphql.schema.diffing.ana.SchemaDifference.UnionMemberDeletion +import static graphql.schema.diffing.ana.SchemaDifference.UnionModification + +class EditOperationAnalyzerTest extends Specification { + + def "object renamed"() { + given: + def oldSdl = ''' + type Query { + foo: String + } + ''' + def newSdl = ''' + schema { + query: MyQuery + } + type MyQuery { + foo: String + } + + ''' + when: + def changes = calcDiff(oldSdl, newSdl) + then: + changes.objectDifferences["Query"] === changes.objectDifferences["MyQuery"] + changes.objectDifferences["Query"] instanceof ObjectModification + (changes.objectDifferences["Query"] as ObjectModification).oldName == "Query" + (changes.objectDifferences["Query"] as ObjectModification).newName == "MyQuery" + } + + def "interface renamed"() { + given: + def oldSdl = ''' + type Query implements I { + foo: String + } + interface I { + foo: String + } + ''' + def newSdl = ''' + type Query implements IRenamed { + foo: String + } + interface IRenamed { + foo: String + } + + ''' + when: + def changes = calcDiff(oldSdl, newSdl) + then: + changes.interfaceDifferences["I"] === changes.interfaceDifferences["IRenamed"] + changes.interfaceDifferences["I"] instanceof InterfaceModification + (changes.interfaceDifferences["I"] as InterfaceModification).oldName == "I" + (changes.interfaceDifferences["I"] as InterfaceModification).newName == "IRenamed" + } + + def "interface removed from object"() { + given: + def oldSdl = ''' + type Query implements I { + foo: String + } + interface I { + foo: String + } + ''' + def newSdl = ''' + type Query{ + foo: String + } + ''' + when: + def changes = calcDiff(oldSdl, newSdl) + then: + changes.objectDifferences["Query"] instanceof ObjectModification + def implementationDeletions = (changes.objectDifferences["Query"] as ObjectModification).getDetails(ObjectInterfaceImplementationDeletion) + implementationDeletions[0].name == "I" + } + + def "interface removed from interface"() { + given: + def oldSdl = ''' + type Query { + foo: Foo + } + interface FooI { + foo: String + } + interface Foo implements FooI { + foo: String + } + type FooImpl implements Foo & FooI { + foo: String + } + ''' + def newSdl = ''' + type Query { + foo: Foo + } + interface Foo { + foo: String + } + type FooImpl implements Foo { + foo: String + } + ''' + when: + def changes = calcDiff(oldSdl, newSdl) + then: + changes.interfaceDifferences["Foo"] instanceof InterfaceModification + def implementationDeletions = (changes.interfaceDifferences["Foo"] as InterfaceModification).getDetails(InterfaceInterfaceImplementationDeletion) + implementationDeletions[0].name == "FooI" + } + + def "object and interface field renamed"() { + given: + def oldSdl = ''' + type Query implements I{ + hello: String + } + interface I { + hello: String + } + ''' + def newSdl = ''' + type Query implements I{ + hello2: String + } + interface I { + hello2: String + } + ''' + when: + def changes = calcDiff(oldSdl, newSdl) + then: + changes.objectDifferences["Query"] instanceof ObjectModification + def objectModification = changes.objectDifferences["Query"] as ObjectModification + def oFieldRenames = objectModification.getDetails(ObjectFieldRename.class) + oFieldRenames[0].oldName == "hello" + oFieldRenames[0].newName == "hello2" + and: + changes.interfaceDifferences["I"] instanceof InterfaceModification + def interfaceModification = changes.interfaceDifferences["I"] as InterfaceModification + def iFieldRenames = interfaceModification.getDetails(InterfaceFieldRename.class) + iFieldRenames[0].oldName == "hello" + iFieldRenames[0].newName == "hello2" + + } + + def "object and interface field deleted"() { + given: + def oldSdl = ''' + type Query implements I{ + hello: String + toDelete: String + } + interface I { + hello: String + toDelete: String + } + ''' + def newSdl = ''' + type Query implements I{ + hello: String + } + interface I { + hello: String + } + ''' + when: + def changes = calcDiff(oldSdl, newSdl) + then: + changes.objectDifferences["Query"] instanceof ObjectModification + def objectModification = changes.objectDifferences["Query"] as ObjectModification + def oFieldDeletions = objectModification.getDetails(ObjectFieldDeletion.class) + oFieldDeletions[0].name == "toDelete" + and: + changes.interfaceDifferences["I"] instanceof InterfaceModification + def interfaceModification = changes.interfaceDifferences["I"] as InterfaceModification + def iFieldDeletions = interfaceModification.getDetails(InterfaceFieldDeletion.class) + iFieldDeletions[0].name == "toDelete" + + } + + def "union added"() { + given: + def oldSdl = ''' + type Query { + hello: String + } + ''' + def newSdl = ''' + type Query { + hello: String + u: U + } + union U = A | B + type A { + foo: String + } + type B { + foo: String + } + + ''' + when: + def changes = calcDiff(oldSdl, newSdl) + then: + changes.unionDifferences["U"] instanceof UnionAddition + (changes.unionDifferences["U"] as UnionAddition).name == "U" + } + + def "union deleted"() { + given: + def oldSdl = ''' + type Query { + hello: String + u: U + } + union U = A | B + type A { + foo: String + } + type B { + foo: String + } + ''' + def newSdl = ''' + type Query { + hello: String + } + ''' + when: + def changes = calcDiff(oldSdl, newSdl) + then: + changes.unionDifferences["U"] instanceof UnionDeletion + (changes.unionDifferences["U"] as UnionDeletion).name == "U" + } + + def "union renamed"() { + given: + def oldSdl = ''' + type Query { + u: U + } + union U = A | B + type A { + foo: String + } + type B { + foo: String + } + ''' + def newSdl = ''' + type Query { + u: X + } + union X = A | B + type A { + foo: String + } + type B { + foo: String + } + ''' + when: + def changes = calcDiff(oldSdl, newSdl) + then: + changes.unionDifferences["X"] === changes.unionDifferences["U"] + changes.unionDifferences["U"] instanceof UnionModification + (changes.unionDifferences["U"] as UnionModification).oldName == "U" + (changes.unionDifferences["U"] as UnionModification).newName == "X" + } + + def "union renamed and member removed"() { + given: + def oldSdl = ''' + type Query { + u: U + } + union U = A | B + type A { + foo: String + } + type B { + foo: String + } + ''' + def newSdl = ''' + type Query { + u: X + } + union X = A + type A { + foo: String + } + ''' + when: + def changes = calcDiff(oldSdl, newSdl) + then: + changes.unionDifferences["U"] instanceof UnionModification + def unionDiff = changes.unionDifferences["U"] as UnionModification + unionDiff.oldName == "U" + unionDiff.newName == "X" + unionDiff.getDetails(UnionMemberDeletion)[0].name == "B" + } + + def "union renamed and member added"() { + given: + def oldSdl = ''' + type Query { + u: U + } + union U = A + type A { + foo: String + } + + ''' + def newSdl = ''' + type Query { + u: X + } + union X = A | B + type A { + foo: String + } + type B { + foo: String + } + ''' + when: + def changes = calcDiff(oldSdl, newSdl) + then: + changes.unionDifferences["U"] instanceof UnionModification + def unionDiff = changes.unionDifferences["U"] as UnionModification + unionDiff.oldName == "U" + unionDiff.newName == "X" + unionDiff.getDetails(UnionMemberAddition)[0].name == "B" + } + + def "union member added"() { + given: + def oldSdl = ''' + type Query { + hello: String + u: U + } + union U = A | B + type A { + foo: String + } + type B { + foo: String + } + ''' + def newSdl = ''' + type Query { + hello: String + u: U + } + union U = A | B | C + type A { + foo: String + } + type B { + foo: String + } + type C { + foo: String + } + + ''' + when: + def changes = calcDiff(oldSdl, newSdl) + then: + changes.unionDifferences["U"] instanceof UnionModification + def unionModification = changes.unionDifferences["U"] as UnionModification + unionModification.getDetails(UnionMemberAddition)[0].name == "C" + } + + def "union member deleted"() { + given: + def oldSdl = ''' + type Query { + hello: String + u: U + } + union U = A | B + type A { + foo: String + } + type B { + foo: String + } + ''' + def newSdl = ''' + type Query { + hello: String + u: U + } + union U = A + type A { + foo: String + } + ''' + when: + def changes = calcDiff(oldSdl, newSdl) + then: + changes.unionDifferences["U"] instanceof UnionModification + def unionModification = changes.unionDifferences["U"] as UnionModification + unionModification.getDetails(UnionMemberDeletion)[0].name == "B" + } + + def "field type modified"() { + given: + def oldSdl = ''' + type Query { + hello: String + } + ''' + def newSdl = ''' + type Query { + hello: String! + } + ''' + when: + def changes = calcDiff(oldSdl, newSdl) + then: + changes.objectDifferences["Query"] instanceof ObjectModification + def objectModification = changes.objectDifferences["Query"] as ObjectModification + def typeModification = objectModification.getDetails(ObjectFieldTypeModification.class) + typeModification[0].oldType == "String" + typeModification[0].newType == "String!" + } + + def "object and interface field argument added"() { + given: + def oldSdl = ''' + type Query implements I{ + hello: String + } + interface I { + hello: String + } + ''' + def newSdl = ''' + type Query implements I{ + hello(arg: String): String + } + interface I { + hello(arg: String): String + } + ''' + when: + def changes = calcDiff(oldSdl, newSdl) + then: + changes.objectDifferences["Query"] instanceof ObjectModification + def objectModification = changes.objectDifferences["Query"] as ObjectModification + def objectArgumentAdded = objectModification.getDetails(ObjectFieldArgumentAddition.class); + objectArgumentAdded[0].name == "arg" + and: + changes.interfaceDifferences["I"] instanceof InterfaceModification + def interfaceModification = changes.interfaceDifferences["I"] as InterfaceModification + def interfaceArgumentAdded = interfaceModification.getDetails(InterfaceFieldArgumentAddition.class); + interfaceArgumentAdded[0].name == "arg" + + } + + def "object and interface field argument renamed"() { + given: + def oldSdl = ''' + type Query implements I{ + hello(arg: String): String + } + interface I { + hello(arg: String): String + } + ''' + def newSdl = ''' + type Query implements I{ + hello(argRename: String): String + } + interface I { + hello(argRename: String): String + } + ''' + when: + def changes = calcDiff(oldSdl, newSdl) + then: + changes.objectDifferences["Query"] instanceof ObjectModification + def objectModification = changes.objectDifferences["Query"] as ObjectModification + def objectArgumentRenamed = objectModification.getDetails(ObjectFieldArgumentRename.class); + objectArgumentRenamed[0].oldName == "arg" + objectArgumentRenamed[0].newName == "argRename" + and: + changes.interfaceDifferences["I"] instanceof InterfaceModification + def interfaceModification = changes.interfaceDifferences["I"] as InterfaceModification + def interfaceArgumentRenamed = interfaceModification.getDetails(InterfaceFieldArgumentRename.class); + interfaceArgumentRenamed[0].oldName == "arg" + interfaceArgumentRenamed[0].newName == "argRename" + + } + + + def "object and interface field argument deleted"() { + given: + def oldSdl = ''' + type Query implements I{ + hello(arg: String): String + } + interface I{ + hello(arg: String): String + } + ''' + def newSdl = ''' + type Query implements I { + hello: String + } + interface I { + hello: String + } + ''' + when: + def changes = calcDiff(oldSdl, newSdl) + then: + changes.objectDifferences["Query"] instanceof ObjectModification + def objectModification = changes.objectDifferences["Query"] as ObjectModification + def oArgumentRemoved = objectModification.getDetails(ObjectFieldArgumentDeletion.class); + oArgumentRemoved[0].fieldName == "hello" + oArgumentRemoved[0].name == "arg" + and: + changes.interfaceDifferences["I"] instanceof InterfaceModification + def interfaceModification = changes.interfaceDifferences["I"] as InterfaceModification + def iArgumentRemoved = interfaceModification.getDetails(InterfaceFieldArgumentDeletion.class); + iArgumentRemoved[0].fieldName == "hello" + iArgumentRemoved[0].name == "arg" + + } + + def "argument default value modified for Object and Interface"() { + given: + def oldSdl = ''' + type Query implements Foo { + foo(arg: String = "bar"): String + } + interface Foo { + foo(arg: String = "bar"): String + } + + ''' + def newSdl = ''' + type Query implements Foo { + foo(arg: String = "barChanged"): String + } + interface Foo { + foo(arg: String = "barChanged"): String + } + ''' + when: + def changes = calcDiff(oldSdl, newSdl) + + then: + changes.objectDifferences["Query"] instanceof ObjectModification + def objectModification = changes.objectDifferences["Query"] as ObjectModification + def objDefaultValueModified = objectModification.getDetails(ObjectFieldArgumentDefaultValueModification.class); + objDefaultValueModified[0].fieldName == "foo" + objDefaultValueModified[0].argumentName == "arg" + objDefaultValueModified[0].oldValue == '"bar"' + objDefaultValueModified[0].newValue == '"barChanged"' + and: + changes.interfaceDifferences["Foo"] instanceof InterfaceModification + def interfaceModification = changes.interfaceDifferences["Foo"] as InterfaceModification + def intDefaultValueModified = interfaceModification.getDetails(InterfaceFieldArgumentDefaultValueModification.class); + intDefaultValueModified[0].fieldName == "foo" + intDefaultValueModified[0].argumentName == "arg" + intDefaultValueModified[0].oldValue == '"bar"' + intDefaultValueModified[0].newValue == '"barChanged"' + } + + def "object and interface argument type changed completely"() { + given: + def oldSdl = ''' + type Query implements Foo { + foo(arg: String ): String + } + interface Foo { + foo(arg: String): String + } + + ''' + def newSdl = ''' + type Query implements Foo { + foo(arg: Int!): String + } + interface Foo { + foo(arg: Int!): String + } + ''' + when: + def changes = calcDiff(oldSdl, newSdl) + + then: + changes.objectDifferences["Query"] instanceof ObjectModification + def objectModification = changes.objectDifferences["Query"] as ObjectModification + def objDefaultValueModified = objectModification.getDetails(ObjectFieldArgumentTypeModification.class); + objDefaultValueModified[0].fieldName == "foo" + objDefaultValueModified[0].argumentName == "arg" + objDefaultValueModified[0].oldType == 'String' + objDefaultValueModified[0].newType == 'Int!' + and: + changes.interfaceDifferences["Foo"] instanceof InterfaceModification + def interfaceModification = changes.interfaceDifferences["Foo"] as InterfaceModification + def intDefaultValueModified = interfaceModification.getDetails(InterfaceFieldArgumentTypeModification.class); + intDefaultValueModified[0].fieldName == "foo" + intDefaultValueModified[0].argumentName == "arg" + intDefaultValueModified[0].oldType == 'String' + intDefaultValueModified[0].newType == 'Int!' + } + + def "object and interface argument type changed wrapping type"() { + given: + def oldSdl = ''' + type Query implements Foo { + foo(arg: String ): String + } + interface Foo { + foo(arg: String): String + } + + ''' + def newSdl = ''' + type Query implements Foo { + foo(arg: String!): String + } + interface Foo { + foo(arg: String!): String + } + ''' + when: + def changes = calcDiff(oldSdl, newSdl) + + then: + changes.objectDifferences["Query"] instanceof ObjectModification + def objectModification = changes.objectDifferences["Query"] as ObjectModification + def objDefaultValueModified = objectModification.getDetails(ObjectFieldArgumentTypeModification.class); + objDefaultValueModified[0].fieldName == "foo" + objDefaultValueModified[0].argumentName == "arg" + objDefaultValueModified[0].oldType == 'String' + objDefaultValueModified[0].newType == 'String!' + and: + changes.interfaceDifferences["Foo"] instanceof InterfaceModification + def interfaceModification = changes.interfaceDifferences["Foo"] as InterfaceModification + def intDefaultValueModified = interfaceModification.getDetails(InterfaceFieldArgumentTypeModification.class); + intDefaultValueModified[0].fieldName == "foo" + intDefaultValueModified[0].argumentName == "arg" + intDefaultValueModified[0].oldType == 'String' + intDefaultValueModified[0].newType == 'String!' + } + + def "object and interface field added"() { + given: + def oldSdl = ''' + type Query implements I{ + hello: String + } + interface I { + hello: String + } + ''' + def newSdl = ''' + type Query implements I{ + hello: String + newOne: String + } + interface I { + hello: String + newOne: String + } + ''' + when: + def changes = calcDiff(oldSdl, newSdl) + then: + changes.objectDifferences["Query"] instanceof ObjectModification + def objectModification = changes.objectDifferences["Query"] as ObjectModification + def oFieldAdded = objectModification.getDetails(ObjectFieldAddition) + oFieldAdded[0].name == "newOne" + and: + changes.interfaceDifferences["I"] instanceof InterfaceModification + def iInterfaces = changes.interfaceDifferences["I"] as InterfaceModification + def iFieldAdded = iInterfaces.getDetails(InterfaceFieldAddition) + iFieldAdded[0].name == "newOne" + + } + + def "object added"() { + given: + def oldSdl = ''' + type Query { + hello: String + } + ''' + def newSdl = ''' + type Query { + hello: String + foo: Foo + } + type Foo { + id: ID + } + ''' + when: + def changes = calcDiff(oldSdl, newSdl) + then: + changes.objectDifferences["Foo"] instanceof ObjectAddition + } + + def "object removed and field type changed"() { + given: + def oldSdl = ''' + type Query { + foo: Foo + } + type Foo { + bar: String + } + ''' + def newSdl = ''' + type Query { + foo: String + } + ''' + when: + def changes = calcDiff(oldSdl, newSdl) + then: + changes.objectDifferences["Foo"] instanceof ObjectDeletion + (changes.objectDifferences["Foo"] as ObjectDeletion).name == "Foo" + changes.objectDifferences["Query"] instanceof ObjectModification + def queryObjectModification = changes.objectDifferences["Query"] as ObjectModification + queryObjectModification.details.size() == 1 + queryObjectModification.details[0] instanceof ObjectFieldTypeModification + (queryObjectModification.details[0] as ObjectFieldTypeModification).oldType == "Foo" + (queryObjectModification.details[0] as ObjectFieldTypeModification).newType == "String" + + } + + def "Interface and Object field type changed completely"() { + given: + def oldSdl = ''' + type Query implements I{ + foo: String + } + interface I { + foo: String + } + ''' + def newSdl = ''' + type Query implements I{ + foo: ID + } + interface I { + foo: ID + } + ''' + when: + def changes = calcDiff(oldSdl, newSdl) + then: + changes.interfaceDifferences["I"] instanceof InterfaceModification + def iModification = changes.interfaceDifferences["I"] as InterfaceModification + def iFieldTypeModifications = iModification.getDetails(InterfaceFieldTypeModification) + iFieldTypeModifications[0].fieldName == "foo" + iFieldTypeModifications[0].oldType == "String" + iFieldTypeModifications[0].newType == "ID" + and: + changes.objectDifferences["Query"] instanceof ObjectModification + def oModification = changes.objectDifferences["Query"] as ObjectModification + def oFieldTypeModifications = oModification.getDetails(ObjectFieldTypeModification) + oFieldTypeModifications[0].fieldName == "foo" + oFieldTypeModifications[0].oldType == "String" + oFieldTypeModifications[0].newType == "ID" + } + + def "Interface and Object field type changed wrapping type"() { + given: + def oldSdl = ''' + type Query implements I{ + foo: String + } + interface I { + foo: String + } + ''' + def newSdl = ''' + type Query implements I{ + foo: [String!] + } + interface I { + foo: [String!] + } + ''' + when: + def changes = calcDiff(oldSdl, newSdl) + then: + changes.interfaceDifferences["I"] instanceof InterfaceModification + def iModification = changes.interfaceDifferences["I"] as InterfaceModification + def iFieldTypeModifications = iModification.getDetails(InterfaceFieldTypeModification) + iFieldTypeModifications[0].fieldName == "foo" + iFieldTypeModifications[0].oldType == "String" + iFieldTypeModifications[0].newType == "[String!]" + and: + changes.objectDifferences["Query"] instanceof ObjectModification + def oModification = changes.objectDifferences["Query"] as ObjectModification + def oFieldTypeModifications = oModification.getDetails(ObjectFieldTypeModification) + oFieldTypeModifications[0].fieldName == "foo" + oFieldTypeModifications[0].oldType == "String" + oFieldTypeModifications[0].newType == "[String!]" + + + } + + def "new Interface introduced"() { + given: + def oldSdl = ''' + type Query { + foo: Foo + } + type Foo { + id: ID! + } + ''' + def newSdl = ''' + type Query { + foo: Foo + } + type Foo implements Node{ + id: ID! + } + interface Node { + id: ID! + } + ''' + when: + def changes = calcDiff(oldSdl, newSdl) + then: + changes.interfaceDifferences.size() == 1 + changes.interfaceDifferences["Node"] instanceof InterfaceAddition + changes.objectDifferences.size() == 1 + changes.objectDifferences["Foo"] instanceof ObjectModification + def objectModification = changes.objectDifferences["Foo"] as ObjectModification + def addedInterfaceDetails = objectModification.getDetails(ObjectInterfaceImplementationAddition.class) + addedInterfaceDetails.size() == 1 + addedInterfaceDetails[0].name == "Node" + } + + def "Object and Interface added"() { + given: + def oldSdl = ''' + type Query { + foo: String + } + ''' + def newSdl = ''' + type Query { + foo: Foo + } + type Foo implements Node{ + id: ID! + } + interface Node { + id: ID! + } + ''' + when: + def changes = calcDiff(oldSdl, newSdl) + then: + changes.interfaceDifferences.size() == 1 + changes.interfaceDifferences["Node"] instanceof InterfaceAddition + changes.objectDifferences.size() == 2 + changes.objectDifferences["Foo"] instanceof ObjectAddition + changes.objectDifferences["Query"] instanceof ObjectModification + (changes.objectDifferences["Query"] as ObjectModification).getDetails()[0] instanceof ObjectFieldTypeModification + } + + def "interfaced renamed"() { + given: + def oldSdl = ''' + type Query { + foo: Foo + } + type Foo implements Node{ + id: ID! + } + interface Node { + id: ID! + } + ''' + def newSdl = ''' + type Query { + foo: Foo + } + interface Node2 { + id: ID! + } + type Foo implements Node2{ + id: ID! + } + ''' + when: + def changes = calcDiff(oldSdl, newSdl) + then: + changes.interfaceDifferences.size() == 2 + changes.interfaceDifferences["Node"] === changes.interfaceDifferences["Node2"] + changes.interfaceDifferences["Node2"] instanceof InterfaceModification + } + + def "interfaced renamed and another interface added to it"() { + given: + def oldSdl = ''' + type Query { + foo: Foo + } + type Foo implements Node{ + id: ID! + } + interface Node { + id: ID! + } + ''' + def newSdl = ''' + type Query { + foo: Foo + } + interface NewI { + hello: String + } + interface Node2 { + id: ID! + } + type Foo implements Node2 & NewI{ + id: ID! + hello: String + } + ''' + when: + def changes = calcDiff(oldSdl, newSdl) + then: + changes.interfaceDifferences.size() == 3 + changes.interfaceDifferences["Node"] == changes.interfaceDifferences["Node2"] + changes.interfaceDifferences["Node2"] instanceof InterfaceModification + changes.interfaceDifferences["NewI"] instanceof InterfaceAddition + changes.objectDifferences.size() == 1 + changes.objectDifferences["Foo"] instanceof ObjectModification + def objectModification = changes.objectDifferences["Foo"] as ObjectModification + def addedInterfaceDetails = objectModification.getDetails(ObjectInterfaceImplementationAddition) + addedInterfaceDetails.size() == 1 + addedInterfaceDetails[0].name == "NewI" + + } + + def "enum renamed"() { + given: + def oldSdl = ''' + type Query { + foo: String + } + enum E { + A, B + } + ''' + def newSdl = ''' + type Query { + foo: ERenamed + } + enum ERenamed { + A, B + } + ''' + when: + def changes = calcDiff(oldSdl, newSdl) + then: + changes.enumDifferences["E"] === changes.enumDifferences["ERenamed"] + def modification = changes.enumDifferences["E"] as EnumModification + modification.oldName == "E" + modification.newName == "ERenamed" + + } + + def "enum added"() { + given: + def oldSdl = ''' + type Query { + foo: String + } + ''' + def newSdl = ''' + type Query { + foo: E + } + enum E { + A, B + } + ''' + when: + def changes = calcDiff(oldSdl, newSdl) + then: + changes.enumDifferences["E"] instanceof EnumAddition + (changes.enumDifferences["E"] as EnumAddition).getName() == "E" + } + + def "enum deleted"() { + given: + def oldSdl = ''' + type Query { + foo: E + } + enum E { + A, B + } + ''' + def newSdl = ''' + type Query { + foo: String + } + ''' + when: + def changes = calcDiff(oldSdl, newSdl) + then: + changes.enumDifferences["E"] instanceof EnumDeletion + (changes.enumDifferences["E"] as EnumDeletion).getName() == "E" + } + + + def "enum value added"() { + given: + def oldSdl = ''' + type Query { + e: E + } + enum E { + A + } + ''' + def newSdl = ''' + type Query { + e: E + } + enum E { + A, B + } + ''' + when: + def changes = calcDiff(oldSdl, newSdl) + then: + changes.enumDifferences["E"] instanceof EnumModification + def enumModification = changes.enumDifferences["E"] as EnumModification + enumModification.getDetails(EnumValueAddition)[0].name == "B" + } + + def "enum value deleted"() { + given: + def oldSdl = ''' + type Query { + e: E + } + enum E { + A,B + } + ''' + def newSdl = ''' + type Query { + e: E + } + enum E { + A + } + ''' + when: + def changes = calcDiff(oldSdl, newSdl) + then: + changes.enumDifferences["E"] instanceof EnumModification + def enumModification = changes.enumDifferences["E"] as EnumModification + enumModification.getDetails(EnumValueDeletion)[0].name == "B" + } + + def "scalar added"() { + given: + def oldSdl = ''' + type Query { + foo: String + } + ''' + def newSdl = ''' + type Query { + foo: E + } + scalar E + ''' + when: + def changes = calcDiff(oldSdl, newSdl) + then: + changes.scalarDifferences["E"] instanceof ScalarAddition + (changes.scalarDifferences["E"] as ScalarAddition).getName() == "E" + } + + def "scalar deleted"() { + given: + def oldSdl = ''' + type Query { + foo: E + } + scalar E + ''' + def newSdl = ''' + type Query { + foo: String + } + ''' + when: + def changes = calcDiff(oldSdl, newSdl) + then: + changes.scalarDifferences["E"] instanceof ScalarDeletion + (changes.scalarDifferences["E"] as ScalarDeletion).getName() == "E" + } + + def "scalar renamed"() { + given: + def oldSdl = ''' + type Query { + foo: Foo + } + scalar Foo + ''' + def newSdl = ''' + type Query { + foo: Bar + } + scalar Bar + ''' + when: + def changes = calcDiff(oldSdl, newSdl) + then: + changes.scalarDifferences["Foo"] === changes.scalarDifferences["Bar"] + def modification = changes.scalarDifferences["Foo"] as ScalarModification + modification.oldName == "Foo" + modification.newName == "Bar" + } + + def "input object added"() { + given: + def oldSdl = ''' + type Query { + foo: String + } + ''' + def newSdl = ''' + type Query { + foo(arg: I): String + } + input I { + bar: String + } + ''' + when: + def changes = calcDiff(oldSdl, newSdl) + then: + changes.inputObjectDifferences["I"] instanceof InputObjectAddition + (changes.inputObjectDifferences["I"] as InputObjectAddition).getName() == "I" + } + + def "input object field added"() { + given: + def oldSdl = ''' + type Query { + foo(arg: I): String + } + input I { + bar: String + } + ''' + def newSdl = ''' + type Query { + foo(arg: I): String + } + input I { + bar: String + newField: String + } + ''' + when: + def changes = calcDiff(oldSdl, newSdl) + then: + changes.inputObjectDifferences["I"] instanceof InputObjectModification + def modification = changes.inputObjectDifferences["I"] as InputObjectModification + def fieldAddition = modification.getDetails(InputObjectFieldAddition)[0] + fieldAddition.name == "newField" + } + + def "input object field deletion"() { + given: + def oldSdl = ''' + type Query { + foo(arg: I): String + } + input I { + bar: String + toDelete: String + } + ''' + def newSdl = ''' + type Query { + foo(arg: I): String + } + input I { + bar: String + } + ''' + when: + def changes = calcDiff(oldSdl, newSdl) + then: + changes.inputObjectDifferences["I"] instanceof InputObjectModification + def modification = changes.inputObjectDifferences["I"] as InputObjectModification + def fieldDeletion = modification.getDetails(InputObjectFieldDeletion)[0] + fieldDeletion.name == "toDelete" + } + + def "input object field renamed"() { + given: + def oldSdl = ''' + type Query { + foo(arg: I): String + } + input I { + bar: String + } + ''' + def newSdl = ''' + type Query { + foo(arg: I): String + } + input I { + barNew: String + } + ''' + when: + def changes = calcDiff(oldSdl, newSdl) + then: + changes.inputObjectDifferences["I"] instanceof InputObjectModification + def modification = changes.inputObjectDifferences["I"] as InputObjectModification + def fieldDeletion = modification.getDetails(InputObjectFieldRename)[0] + fieldDeletion.oldName == "bar" + fieldDeletion.newName == "barNew" + } + + def "input object field wrapping type changed"() { + given: + def oldSdl = ''' + type Query { + foo(arg: I): String + } + input I { + bar: String + } + ''' + def newSdl = ''' + type Query { + foo(arg: I): String + } + input I { + bar: [String] + } + ''' + when: + def changes = calcDiff(oldSdl, newSdl) + then: + changes.inputObjectDifferences["I"] instanceof InputObjectModification + def modification = changes.inputObjectDifferences["I"] as InputObjectModification + def typeModification = modification.getDetails(InputObjectFieldTypeModification)[0] + typeModification.oldType == "String" + typeModification.newType == "[String]" + } + + def "input object field type changed"() { + given: + def oldSdl = ''' + type Query { + foo(arg: I): String + } + input I { + bar: String + } + ''' + def newSdl = ''' + type Query { + foo(arg: I): String + } + input I { + bar: ID + } + ''' + when: + def changes = calcDiff(oldSdl, newSdl) + then: + changes.inputObjectDifferences["I"] instanceof InputObjectModification + def modification = changes.inputObjectDifferences["I"] as InputObjectModification + def typeModification = modification.getDetails(InputObjectFieldTypeModification)[0] + typeModification.oldType == "String" + typeModification.newType == "ID" + } + + def "input object field default value changed"() { + given: + def oldSdl = ''' + type Query { + foo(arg: I): String + } + input I { + bar: String = "A" + } + ''' + def newSdl = ''' + type Query { + foo(arg: I): String + } + input I { + bar: String = "B" + } + ''' + when: + def changes = calcDiff(oldSdl, newSdl) + then: + changes.inputObjectDifferences["I"] instanceof InputObjectModification + def modification = changes.inputObjectDifferences["I"] as InputObjectModification + def modificationDetail = modification.getDetails(InputObjectFieldDefaultValueModification)[0] + modificationDetail.oldDefaultValue == '"A"' + modificationDetail.newDefaultValue == '"B"' + } + + def "input object deleted"() { + given: + def oldSdl = ''' + type Query { + foo(arg: I): String + } + input I { + bar: String + } + ''' + def newSdl = ''' + type Query { + foo: String + } + ''' + when: + def changes = calcDiff(oldSdl, newSdl) + then: + changes.inputObjectDifferences["I"] instanceof InputObjectDeletion + (changes.inputObjectDifferences["I"] as InputObjectDeletion).getName() == "I" + } + + def "input object renamed"() { + given: + def oldSdl = ''' + type Query { + foo(arg: I): String + } + input I { + bar: String + } + ''' + def newSdl = ''' + type Query { + foo(arg: IRenamed): String + } + input IRenamed { + bar: String + } + ''' + when: + def changes = calcDiff(oldSdl, newSdl) + then: + changes.inputObjectDifferences["I"] === changes.inputObjectDifferences["IRenamed"] + def modification = changes.inputObjectDifferences["I"] as InputObjectModification + modification.oldName == "I" + modification.newName == "IRenamed" + } + + + def "directive added"() { + given: + def oldSdl = ''' + type Query { + foo: String + } + ''' + def newSdl = ''' + type Query { + foo: String + } + directive @d on FIELD + ''' + when: + def changes = calcDiff(oldSdl, newSdl) + then: + changes.directiveDifferences["d"] instanceof DirectiveAddition + (changes.directiveDifferences["d"] as DirectiveAddition).getName() == "d" + } + + def "directive deleted"() { + given: + def oldSdl = ''' + type Query { + foo: String + } + directive @d on FIELD + ''' + def newSdl = ''' + type Query { + foo: String + } + ''' + when: + def changes = calcDiff(oldSdl, newSdl) + then: + changes.directiveDifferences["d"] instanceof DirectiveDeletion + (changes.directiveDifferences["d"] as DirectiveDeletion).getName() == "d" + } + + def "directive renamed"() { + given: + def oldSdl = ''' + type Query { + foo: String + } + directive @d on FIELD + ''' + def newSdl = ''' + type Query { + foo: String + } + directive @dRenamed on FIELD + ''' + when: + def changes = calcDiff(oldSdl, newSdl) + then: + changes.directiveDifferences["d"] === changes.directiveDifferences["dRenamed"] + def modification = changes.directiveDifferences["d"] as DirectiveModification + modification.oldName == "d" + modification.newName == "dRenamed" + } + + def "directive argument renamed"() { + given: + def oldSdl = ''' + type Query { + foo: String + } + directive @d(foo: String) on FIELD + ''' + def newSdl = ''' + type Query { + foo: String + } + directive @d(bar:String) on FIELD + ''' + when: + def changes = calcDiff(oldSdl, newSdl) + then: + changes.directiveDifferences["d"] instanceof DirectiveModification + def renames = (changes.directiveDifferences["d"] as DirectiveModification).getDetails(DirectiveArgumentRename) + renames[0].oldName == "foo" + renames[0].newName == "bar" + + } + + def "directive argument added"() { + given: + def oldSdl = ''' + type Query { + foo: String + } + directive @d on FIELD + ''' + def newSdl = ''' + type Query { + foo: String + } + directive @d(foo:String) on FIELD + ''' + when: + def changes = calcDiff(oldSdl, newSdl) + then: + changes.directiveDifferences["d"] instanceof DirectiveModification + def addition = (changes.directiveDifferences["d"] as DirectiveModification).getDetails(DirectiveArgumentAddition) + addition[0].name == "foo" + + + } + + def "directive argument deleted"() { + given: + def oldSdl = ''' + type Query { + foo: String + } + directive @d(foo:String) on FIELD + ''' + def newSdl = ''' + type Query { + foo: String + } + directive @d on FIELD + ''' + when: + def changes = calcDiff(oldSdl, newSdl) + then: + changes.directiveDifferences["d"] instanceof DirectiveModification + def deletion = (changes.directiveDifferences["d"] as DirectiveModification).getDetails(DirectiveArgumentDeletion) + deletion[0].name == "foo" + + + } + + def "directive argument default value changed"() { + given: + def oldSdl = ''' + type Query { + foo: String + } + directive @d(foo:String = "A") on FIELD + ''' + def newSdl = ''' + type Query { + foo: String + } + directive @d(foo: String = "B") on FIELD + ''' + when: + def changes = calcDiff(oldSdl, newSdl) + then: + changes.directiveDifferences["d"] instanceof DirectiveModification + def defaultValueChange = (changes.directiveDifferences["d"] as DirectiveModification).getDetails(DirectiveArgumentDefaultValueModification) + defaultValueChange[0].argumentName == "foo" + defaultValueChange[0].oldValue == '"A"' + defaultValueChange[0].newValue == '"B"' + + + } + + def "directive argument type changed completely"() { + given: + def oldSdl = ''' + type Query { + foo: String + } + directive @d(foo:String) on FIELD + ''' + def newSdl = ''' + type Query { + foo: String + } + directive @d(foo: ID) on FIELD + ''' + when: + def changes = calcDiff(oldSdl, newSdl) + then: + changes.directiveDifferences["d"] instanceof DirectiveModification + def argTypeModification = (changes.directiveDifferences["d"] as DirectiveModification).getDetails(DirectiveArgumentTypeModification) + argTypeModification[0].argumentName == "foo" + argTypeModification[0].oldType == 'String' + argTypeModification[0].newType == 'ID' + } + + def "directive argument wrapping type changed"() { + given: + def oldSdl = ''' + type Query { + foo: String + } + directive @d(foo:String) on FIELD + ''' + def newSdl = ''' + type Query { + foo: String + } + directive @d(foo: [String]!) on FIELD + ''' + when: + def changes = calcDiff(oldSdl, newSdl) + then: + changes.directiveDifferences["d"] instanceof DirectiveModification + def argTypeModification = (changes.directiveDifferences["d"] as DirectiveModification).getDetails(DirectiveArgumentTypeModification) + argTypeModification[0].argumentName == "foo" + argTypeModification[0].oldType == 'String' + argTypeModification[0].newType == '[String]!' + } + + + + + + EditOperationAnalysisResult calcDiff( + String oldSdl, + String newSdl + ) { + def oldSchema = TestUtil.schema(oldSdl) + def newSchema = TestUtil.schema(newSdl) + def changes = new SchemaDiffing().diffAndAnalyze(oldSchema, newSchema) + return changes + } +} diff --git a/src/test/groovy/graphql/schema/fetching/ConfusedPojo.java b/src/test/groovy/graphql/schema/fetching/ConfusedPojo.java new file mode 100644 index 0000000000..aa0bc174c6 --- /dev/null +++ b/src/test/groovy/graphql/schema/fetching/ConfusedPojo.java @@ -0,0 +1,28 @@ +package graphql.schema.fetching; + +public class ConfusedPojo { + + public String getRecordLike() { + return "getRecordLike"; + } + + public String recordLike() { + return "recordLike"; + } + + public String gettingConfused() { + return "gettingConfused"; + } + + public String getTingConfused() { + return "getTingConfused"; + } + + public boolean issues() { + return true; + } + + public boolean isSues() { + return false; + } +} diff --git a/src/test/groovy/graphql/schema/fetching/LambdaFetchingSupportTest.groovy b/src/test/groovy/graphql/schema/fetching/LambdaFetchingSupportTest.groovy new file mode 100644 index 0000000000..d3a68f992d --- /dev/null +++ b/src/test/groovy/graphql/schema/fetching/LambdaFetchingSupportTest.groovy @@ -0,0 +1,144 @@ +package graphql.schema.fetching + +import spock.lang.Specification + +class LambdaFetchingSupportTest extends Specification { + + def "can proxy Pojo methods"() { + + def pojo = new Pojo("Brad", 42) + when: + def getName = LambdaFetchingSupport.mkCallFunction(Pojo.class, "getName", String.class) + def getAge = LambdaFetchingSupport.mkCallFunction(Pojo.class, "getAge", Integer.TYPE) + + then: + getName.apply(pojo) == "Brad" + getAge.apply(pojo) == 42 + } + + def "get make getters based on property names"() { + def pojo = new Pojo("Brad", 42) + when: + def getter = LambdaFetchingSupport.createGetter(Pojo.class, "name") + then: + getter.isPresent() + getter.get().apply(pojo) == "Brad" + + when: + getter = LambdaFetchingSupport.createGetter(Pojo.class, "age") + then: + getter.isPresent() + getter.get().apply(pojo) == 42 + + } + + def "get make getters based on record like names"() { + def pojo = new Pojo("Brad", 42) + when: + def getter = LambdaFetchingSupport.createGetter(Pojo.class, "recordLike") + then: + getter.isPresent() + getter.get().apply(pojo) == "recordLike" + + // + // record like getters will be found first - this is new behavior but more sensible behavior + def confusedPojo = new ConfusedPojo() + when: + getter = LambdaFetchingSupport.createGetter(ConfusedPojo.class, "recordLike") + then: + getter.isPresent() + getter.get().apply(confusedPojo) == "recordLike" + + // weird arse getter methods like `issues` versus `isSues` + when: + getter = LambdaFetchingSupport.createGetter(ConfusedPojo.class, "gettingConfused") + then: + getter.isPresent() + getter.get().apply(confusedPojo) == "gettingConfused" + + when: + getter = LambdaFetchingSupport.createGetter(ConfusedPojo.class, "tingConfused") + then: + getter.isPresent() + getter.get().apply(confusedPojo) == "getTingConfused" + + // weird arse getter methods like `issues` versus `isSues` + when: + getter = LambdaFetchingSupport.createGetter(ConfusedPojo.class, "issues") + then: + getter.isPresent() + getter.get().apply(confusedPojo) == true + + when: + getter = LambdaFetchingSupport.createGetter(ConfusedPojo.class, "sues") + then: + getter.isPresent() + getter.get().apply(confusedPojo) == false + + } + + def "will handle missing ones"() { + + when: + def getter = LambdaFetchingSupport.createGetter(Pojo.class, "nameX") + then: + !getter.isPresent() + } + + def "will handle weird ones"() { + + def pojo = new Pojo("Brad", 42) + + when: + def getter = LambdaFetchingSupport.createGetter(Pojo.class, "get") + then: + getter.isPresent() + getter.get().apply(pojo) == "get" + + when: + getter = LambdaFetchingSupport.createGetter(Pojo.class, "is") + then: + getter.isPresent() + getter.get().apply(pojo) == "is" + } + + def "can handle boolean setters - is by preference"() { + + def pojo = new Pojo("Brad", 42) + when: + def getter = LambdaFetchingSupport.createGetter(Pojo.class, "interesting") + then: + getter.isPresent() + getter.get().apply(pojo) == true + + when: + getter = LambdaFetchingSupport.createGetter(Pojo.class, "alone") + then: + getter.isPresent() + getter.get().apply(pojo) == true + + when: + getter = LambdaFetchingSupport.createGetter(Pojo.class, "booleanAndNullish") + then: + getter.isPresent() + getter.get().apply(pojo) == null + } + + def "will ignore non public methods"() { + + when: + def getter = LambdaFetchingSupport.createGetter(Pojo.class, "protectedLevelMethod") + then: + !getter.isPresent() + + when: + getter = LambdaFetchingSupport.createGetter(Pojo.class, "privateLevelMethod") + then: + !getter.isPresent() + + when: + getter = LambdaFetchingSupport.createGetter(Pojo.class, "packageLevelMethod") + then: + !getter.isPresent() + } +} diff --git a/src/test/groovy/graphql/schema/fetching/Pojo.java b/src/test/groovy/graphql/schema/fetching/Pojo.java new file mode 100644 index 0000000000..dac6ce914b --- /dev/null +++ b/src/test/groovy/graphql/schema/fetching/Pojo.java @@ -0,0 +1,72 @@ +package graphql.schema.fetching; + +import com.google.common.collect.ImmutableList; + +import java.util.List; + +public class Pojo { + final String name; + final int age; + + public Pojo(String name, int age) { + this.name = name; + this.age = age; + } + + public String getName() { + return name; + } + + public int getAge() { + return age; + } + + public Integer getHeight() { + return null; + } + + public List getOtherNames() { + return ImmutableList.of("A", "B"); + } + + protected String protectedLevelMethod() { + return "protectedLevelMethod"; + } + + private String privateLevelMethod() { + return "privateLevelMethod"; + } + + String packageLevelMethod() { + return "packageLevelMethod"; + } + + public boolean getInteresting() { + return false; + } + + public boolean isInteresting() { + return true; + } + + public Boolean getAlone() { + return true; + } + + public Boolean getBooleanAndNullish() { + return null; + } + + public String get() { + return "get"; + } + + public String is() { + return "is"; + } + + public String recordLike() { + return "recordLike"; + } + +} \ No newline at end of file diff --git a/src/test/groovy/graphql/schema/idl/SchemaGeneratorAppliedDirectiveHelperTest.groovy b/src/test/groovy/graphql/schema/idl/SchemaGeneratorAppliedDirectiveHelperTest.groovy index 49da26d07c..0214a6a86f 100644 --- a/src/test/groovy/graphql/schema/idl/SchemaGeneratorAppliedDirectiveHelperTest.groovy +++ b/src/test/groovy/graphql/schema/idl/SchemaGeneratorAppliedDirectiveHelperTest.groovy @@ -2,7 +2,6 @@ package graphql.schema.idl import graphql.TestUtil -import graphql.schema.GraphQLArgument import graphql.schema.GraphQLInputType import spock.lang.Specification @@ -69,41 +68,22 @@ class SchemaGeneratorAppliedDirectiveHelperTest extends Specification { barType.directives.collect { it.name }.sort() == ["foo"] barType.appliedDirectives.collect { it.name }.sort() == ["foo"] - - def fooDirective = field.getDirective("foo") - fooDirective.arguments.collect { it.name }.sort() == ["arg1", "arg2"] - fooDirective.arguments.collect { GraphQLArgument.getArgumentValue(it) }.sort() == ["arg2Value", "fooArg1Value",] - def fooAppliedDirective = field.getAppliedDirective("foo") fooAppliedDirective.arguments.collect { it.name }.sort() == ["arg1", "arg2"] fooAppliedDirective.arguments.collect { it.getValue() }.sort() == ["arg2Value", "fooArg1Value"] - - def fooDirectiveOnType = barType.getDirective("foo") - fooDirectiveOnType.arguments.collect { it.name }.sort() == ["arg1", "arg2"] - fooDirectiveOnType.arguments.collect { GraphQLArgument.getArgumentValue(it) }.sort() == ["BarTypeValue", "arg2Value",] - def fooAppliedDirectiveOnType = barType.getAppliedDirective("foo") fooAppliedDirectiveOnType.arguments.collect { it.name }.sort() == ["arg1", "arg2"] fooAppliedDirectiveOnType.arguments.collect { it.getValue() }.sort() == ["BarTypeValue", "arg2Value",] - - def complexDirective = complexField.getDirective("complex") - complexDirective.arguments.collect { it.name }.sort() == ["complexArg1"] - complexDirective.arguments.collect { GraphQLArgument.getArgumentValue(it) }.sort() == [ - [name:"Boris", address:[number:10, street:"Downing St", town:"London"]] - ] - def complexAppliedDirective = complexField.getAppliedDirective("complex") GraphQLInputType complexInputType = schema.getTypeAs("ComplexInput") complexAppliedDirective.arguments.collect { it.name }.sort() == ["complexArg1"] complexAppliedDirective.arguments.collect { it.getValue() }.sort() == [ [name:"Boris", address:[number:10, street:"Downing St", town:"London"]] ] - } - def "can capture ONLY applied directives"() { when: diff --git a/src/test/groovy/graphql/schema/idl/SchemaGeneratorDirectiveHelperTest.groovy b/src/test/groovy/graphql/schema/idl/SchemaGeneratorDirectiveHelperTest.groovy index 9787fd3d14..e54df617c3 100644 --- a/src/test/groovy/graphql/schema/idl/SchemaGeneratorDirectiveHelperTest.groovy +++ b/src/test/groovy/graphql/schema/idl/SchemaGeneratorDirectiveHelperTest.groovy @@ -2,6 +2,7 @@ package graphql.schema.idl import graphql.ExecutionInput import graphql.GraphQL +import graphql.GraphQLContext import graphql.execution.ValuesResolver import graphql.schema.Coercing import graphql.schema.CoercingParseLiteralException @@ -10,9 +11,10 @@ import graphql.schema.CoercingSerializeException import graphql.schema.DataFetcher import graphql.schema.DataFetchingEnvironment import graphql.schema.FieldCoordinates +import graphql.schema.GraphQLAppliedDirective import graphql.schema.GraphQLArgument import graphql.schema.GraphQLCodeRegistry -import graphql.schema.GraphQLDirective +import graphql.schema.GraphQLDirectiveContainer import graphql.schema.GraphQLEnumType import graphql.schema.GraphQLEnumValueDefinition import graphql.schema.GraphQLFieldDefinition @@ -20,6 +22,7 @@ import graphql.schema.GraphQLFieldsContainer import graphql.schema.GraphQLInputObjectField import graphql.schema.GraphQLInputObjectType import graphql.schema.GraphQLInterfaceType +import graphql.schema.GraphQLNamedType import graphql.schema.GraphQLObjectType import graphql.schema.GraphQLScalarType import graphql.schema.GraphQLUnionType @@ -109,14 +112,12 @@ class SchemaGeneratorDirectiveHelperTest extends Specification { indirectInputField1 : InputType @inputFieldDirective(target : "indirectInputField1") } - enum EnumType @enumDirective(target:"EnumType") { enumVal1 @enumValueDirective(target : "enumVal1") enumVal2 @enumValueDirective(target : "enumVal2") } scalar ScalarType @scalarDirective(target:"ScalarType") - ''' //`this contains the name of the element that was asked to be directive wired @@ -141,9 +142,10 @@ class SchemaGeneratorDirectiveHelperTest extends Specification { fieldsContainers[name] = environment.getFieldsContainer()?.getName() fieldDefinitions[name] = environment.getFieldDefinition()?.getName() - GraphQLDirective directive = environment.getDirective() - def arg = directive.getArgument("target") - String target = ValuesResolver.valueToInternalValue(arg.getArgumentValue(), arg.getType()) + GraphQLAppliedDirective appliedDirective = environment.getAppliedDirective() + def arg = appliedDirective.getArgument("target") + + String target = ValuesResolver.valueToInternalValue(arg.getArgumentValue(), arg.getType(), GraphQLContext.getDefault(), Locale.getDefault()) assert name == target, " The target $target is not equal to the object name $name" return element } @@ -327,7 +329,7 @@ class SchemaGeneratorDirectiveHelperTest extends Specification { // we use the non shortcut path to the data fetcher here so prove it still works def fetcher = directiveEnv.getCodeRegistry().getDataFetcher(directiveEnv.fieldsContainer, field) def newFetcher = wrapDataFetcher(fetcher, { dfEnv, value -> - def directiveName = directiveEnv.directive.name + def directiveName = directiveEnv.appliedDirective.name if (directiveName == "uppercase") { return String.valueOf(value).toUpperCase() } else if (directiveName == "lowercase") { @@ -619,7 +621,7 @@ class SchemaGeneratorDirectiveHelperTest extends Specification { def namedWiring = new SchemaDirectiveWiring() { @Override GraphQLFieldDefinition onField(SchemaDirectiveWiringEnvironment environment) { - GraphQLDirective directive = environment.getDirective() + GraphQLAppliedDirective directive = environment.getAppliedDirective() DataFetcher existingFetcher = environment.getFieldDataFetcher() DataFetcher newDF = new DataFetcher() { @@ -683,7 +685,7 @@ class SchemaGeneratorDirectiveHelperTest extends Specification { @Override GraphQLFieldDefinition onField(SchemaDirectiveWiringEnvironment env) { generalCount++ - def directiveNames = env.getDirectives().values().collect { d -> d.getName() }.sort() + def directiveNames = env.getAppliedDirectives().values().collect { d -> d.getName() }.sort() assert directiveNames == ["factoryDirective", "generalDirective", "namedDirective1", "namedDirective2"] return env.getFieldDefinition() } @@ -693,7 +695,7 @@ class SchemaGeneratorDirectiveHelperTest extends Specification { @Override GraphQLFieldDefinition onField(SchemaDirectiveWiringEnvironment env) { factoryCount++ - def directiveNames = env.getDirectives().values().collect { d -> d.getName() }.sort() + def directiveNames = env.getAppliedDirectives().values().collect { d -> d.getName() }.sort() assert directiveNames == ["factoryDirective", "generalDirective", "namedDirective1", "namedDirective2"] return env.getFieldDefinition() } @@ -715,10 +717,10 @@ class SchemaGeneratorDirectiveHelperTest extends Specification { @Override GraphQLFieldDefinition onField(SchemaDirectiveWiringEnvironment env) { namedCount++ - def directiveNames = env.getDirectives().values().collect { d -> d.getName() }.sort() + def directiveNames = env.getAppliedDirectives().values().collect { d -> d.getName() }.sort() assert directiveNames == ["factoryDirective", "generalDirective", "namedDirective1", "namedDirective2"] - assert env.getDirective("factoryDirective") != null + assert env.getAppliedDirective("factoryDirective") != null assert env.containsDirective("factoryDirective") return env.getFieldDefinition() } @@ -775,11 +777,11 @@ class SchemaGeneratorDirectiveHelperTest extends Specification { argCount++ def arg = env.getElement() if (arg.getName() == "arg1") { - assert env.getDirectives().keySet().sort() == ["argDirective1", "argDirective2"] + assert env.getAppliedDirectives().keySet().sort() == ["argDirective1", "argDirective2"] assert env.getAppliedDirectives().keySet().sort() == ["argDirective1", "argDirective2"] } if (arg.getName() == "arg2") { - assert env.getDirectives().keySet().sort() == ["argDirective3"] + assert env.getAppliedDirectives().keySet().sort() == ["argDirective3"] assert env.getAppliedDirectives().keySet().sort() == ["argDirective3"] } def fieldDef = env.getFieldDefinition() @@ -793,7 +795,7 @@ class SchemaGeneratorDirectiveHelperTest extends Specification { GraphQLFieldDefinition onField(SchemaDirectiveWiringEnvironment env) { fieldCount++ - assert env.getDirectives().keySet().sort() == ["fieldDirective"] + assert env.getAppliedDirectives().keySet().sort() == ["fieldDirective"] def fieldDef = env.getFieldDefinition() assert fieldDef.getDirectives().collect({ d -> d.getName() }) == ["fieldDirective"] diff --git a/src/test/groovy/graphql/schema/idl/SchemaGeneratorTest.groovy b/src/test/groovy/graphql/schema/idl/SchemaGeneratorTest.groovy index 4d9f5f7029..a8e9ce3617 100644 --- a/src/test/groovy/graphql/schema/idl/SchemaGeneratorTest.groovy +++ b/src/test/groovy/graphql/schema/idl/SchemaGeneratorTest.groovy @@ -8,6 +8,7 @@ import graphql.schema.DataFetcher import graphql.schema.DataFetcherFactory import graphql.schema.DataFetcherFactoryEnvironment import graphql.schema.DataFetchingEnvironment +import graphql.schema.GraphQLAppliedDirective import graphql.schema.GraphQLArgument import graphql.schema.GraphQLCodeRegistry import graphql.schema.GraphQLDirective @@ -27,7 +28,6 @@ import graphql.schema.GraphQLType import graphql.schema.GraphQLTypeUtil import graphql.schema.GraphQLUnionType import graphql.schema.GraphqlTypeComparatorRegistry -import graphql.schema.PropertyDataFetcher import graphql.schema.idl.errors.NotAnInputTypeError import graphql.schema.idl.errors.NotAnOutputTypeError import graphql.schema.idl.errors.SchemaProblem @@ -1310,8 +1310,7 @@ class SchemaGeneratorTest extends Specification { def fieldDirective2 = field2.getDirectives()[0] fieldDirective2.getName() == "fieldDirective2" - - def directive = type.getDirectives()[3] as GraphQLDirective + def directive = type.getAppliedDirectives()[3] as GraphQLAppliedDirective directive.name == "directiveWithArgs" directive.arguments.size() == 5 @@ -1366,7 +1365,7 @@ class SchemaGeneratorTest extends Specification { expect: - container.getDirective(directiveName) != null + container.getAppliedDirective(directiveName) != null if (container instanceof GraphQLEnumType) { def evd = ((GraphQLEnumType) container).getValue("X").getDirective("EnumValueDirective") @@ -1634,13 +1633,14 @@ class SchemaGeneratorTest extends Specification { argInt.getDirective("thirdDirective") != null def intDirective = argInt.getDirective("intDirective") - intDirective.name == "intDirective" - intDirective.arguments.size() == 1 - def directiveArg = intDirective.getArgument("inception") + def intAppliedDirective = argInt.getAppliedDirective("intDirective") + intAppliedDirective.name == "intDirective" + intAppliedDirective.arguments.size() == 1 + def directiveArg = intAppliedDirective.getArgument("inception") directiveArg.name == "inception" directiveArg.type == GraphQLBoolean printAst(directiveArg.argumentValue.value as Node) == "true" - directiveArg.argumentDefaultValue.value == null + intDirective.getArgument("inception").argumentDefaultValue.value == null } def "directives definitions can be made"() { @@ -1702,13 +1702,15 @@ class SchemaGeneratorTest extends Specification { then: def directiveTest1 = schema.getDirective("test1") GraphQLNonNull.nonNull(GraphQLBoolean).isEqualTo(directiveTest1.getArgument("include").type) - directiveTest1.getArgument("include").argumentValue.value == null + directiveTest1.getArgument("include").argumentDefaultValue.value == null + def appliedDirective1 = schema.getObjectType("Query").getFieldDefinition("f1").getAppliedDirective("test1") + printAst(appliedDirective1.getArgument("include").argumentValue.value as Node) == "false" def directiveTest2 = schema.getDirective("test2") GraphQLNonNull.nonNull(GraphQLBoolean).isEqualTo(directiveTest2.getArgument("include").type) - printAst(directiveTest2.getArgument("include").argumentValue.value as Node) == "true" printAst(directiveTest2.getArgument("include").argumentDefaultValue.value as Node) == "true" - + def appliedDirective2 = schema.getObjectType("Query").getFieldDefinition("f2").getAppliedDirective("test2") + printAst(appliedDirective2.getArgument("include").argumentValue.value as Node) == "true" } def "missing directive arguments are transferred as are default values"() { @@ -1733,16 +1735,17 @@ class SchemaGeneratorTest extends Specification { then: def directive = schema.getObjectType("Query").getFieldDefinition("f").getDirective("testDirective") directive.getArgument("knownArg1").type == GraphQLString - printAst(directive.getArgument("knownArg1").argumentValue.value as Node) == '"overrideVal1"' printAst(directive.getArgument("knownArg1").argumentDefaultValue.value as Node) == '"defaultValue1"' + def appliedDirective = schema.getObjectType("Query").getFieldDefinition("f").getAppliedDirective("testDirective") + printAst(appliedDirective.getArgument("knownArg1").argumentValue.value as Node) == '"overrideVal1"' directive.getArgument("knownArg2").type == GraphQLInt - printAst(directive.getArgument("knownArg2").argumentValue.value as Node) == "666" printAst(directive.getArgument("knownArg2").argumentDefaultValue.value as Node) == "666" + printAst(appliedDirective.getArgument("knownArg2").argumentValue.value as Node) == "666" directive.getArgument("knownArg3").type == GraphQLString - directive.getArgument("knownArg3").argumentValue.value == null directive.getArgument("knownArg3").argumentDefaultValue.value == null + appliedDirective.getArgument("knownArg3").argumentValue.value == null } def "deprecated directive is implicit"() { @@ -1765,60 +1768,29 @@ class SchemaGeneratorTest extends Specification { f1.getDeprecationReason() == "No longer supported" // spec default text def directive = f1.getDirective("deprecated") - directive.name == "deprecated" - directive.getArgument("reason").type == GraphQLString - printAst(directive.getArgument("reason").argumentValue.value as Node) == '"No longer supported"' printAst(directive.getArgument("reason").argumentDefaultValue.value as Node) == '"No longer supported"' directive.validLocations().collect { it.name() } == [Introspection.DirectiveLocation.FIELD_DEFINITION.name()] + def appliedDirective = f1.getAppliedDirective("deprecated") + appliedDirective.name == "deprecated" + appliedDirective.getArgument("reason").type == GraphQLString + printAst(appliedDirective.getArgument("reason").argumentValue.value as Node) == '"No longer supported"' + when: def f2 = schema.getObjectType("Query").getFieldDefinition("f2") then: f2.getDeprecationReason() == "Just because" + def appliedDirective2 = f2.getAppliedDirective("deprecated") + appliedDirective2.name == "deprecated" + appliedDirective2.getArgument("reason").type == GraphQLString + printAst(appliedDirective2.getArgument("reason").argumentValue.value as Node) == '"Just because"' def directive2 = f2.getDirective("deprecated") - directive2.name == "deprecated" - directive2.getArgument("reason").type == GraphQLString - printAst(directive2.getArgument("reason").argumentValue.value as Node) == '"Just because"' printAst(directive2.getArgument("reason").argumentDefaultValue.value as Node) == '"No longer supported"' directive2.validLocations().collect { it.name() } == [Introspection.DirectiveLocation.FIELD_DEFINITION.name()] - - } - - def "@fetch directive is respected if added"() { - def spec = """ - - directive @fetch(from : String!) on FIELD_DEFINITION - - type Query { - name : String, - homePlanet: String @fetch(from : "planetOfBirth") - } - """ - - def wiring = RuntimeWiring.newRuntimeWiring().directiveWiring(new FetchSchemaDirectiveWiring()).build() - def schema = schema(spec, wiring) - - GraphQLObjectType type = schema.getType("Query") as GraphQLObjectType - - expect: - def fetcher = schema.getCodeRegistry().getDataFetcher(type, type.getFieldDefinition("homePlanet")) - fetcher instanceof PropertyDataFetcher - - PropertyDataFetcher propertyDataFetcher = fetcher as PropertyDataFetcher - propertyDataFetcher.getPropertyName() == "planetOfBirth" - // - // no directive - plain name - // - def fetcher2 = schema.getCodeRegistry().getDataFetcher(type, type.getFieldDefinition("name")) - fetcher2 instanceof PropertyDataFetcher - - PropertyDataFetcher propertyDataFetcher2 = fetcher2 as PropertyDataFetcher - propertyDataFetcher2.getPropertyName() == "name" } - def "does not break for circular references to interfaces"() { def spec = """ interface MyInterface { @@ -1859,14 +1831,14 @@ class SchemaGeneratorTest extends Specification { def extraDirective = (GraphQLDirective.newDirective()).name("extra") .argument(GraphQLArgument.newArgument().name("value").type(GraphQLString)).build() - def transformer = new SchemaGeneratorPostProcessing() { + def transformer = new SchemaGeneratorPostProcessing() { // Retained to show deprecated code is still run @Override GraphQLSchema process(GraphQLSchema originalSchema) { originalSchema.transform({ builder -> builder.additionalDirective(extraDirective) }) } } def wiring = RuntimeWiring.newRuntimeWiring() - .transformer(transformer) + .transformer(transformer) // Retained to show deprecated code is still run .build() GraphQLSchema schema = new SchemaGenerator().makeExecutableSchema(types, wiring) expect: @@ -2060,16 +2032,16 @@ class SchemaGeneratorTest extends Specification { schema.getMutationType().name == 'Mutation' when: - def directives = schema.getSchemaDirectives() + def directives = schema.getSchemaDirectives() // Retain for test coverage then: directives.size() == 3 - schema.getSchemaDirective("sd1") != null - schema.getSchemaDirective("sd2") != null - schema.getSchemaDirective("sd3") != null + schema.getSchemaDirective("sd1") != null // Retain for test coverage + schema.getSchemaDirective("sd2") != null // Retain for test coverage + schema.getSchemaDirective("sd3") != null // Retain for test coverage when: - def directivesMap = schema.getSchemaDirectiveByName() + def directivesMap = schema.getSchemaDirectiveByName() // Retain for test coverage then: directives.size() == 3 directivesMap["sd1"] != null @@ -2533,4 +2505,35 @@ class SchemaGeneratorTest extends Specification { then: noExceptionThrown() } + + def "skip and include should be added to the schema only if not already defined"() { + def sdl = ''' + "Directs the executor to skip this field or fragment when the `if`'argument is true." + directive @skip( + "Skipped when true." + if: Boolean! + ) on FIELD | FRAGMENT_SPREAD | INLINE_FRAGMENT + + "Directs the executor to include this field or fragment only when the `if` argument is true" + directive @include( + "Included when true." + if: Boolean! + ) on FIELD | FRAGMENT_SPREAD | INLINE_FRAGMENT + + type Query { + hello: String + } + ''' + when: + def schema = TestUtil.schema(sdl) + then: + schema.getDirectives().findAll { it.name == "skip" }.size() == 1 + schema.getDirectives().findAll { it.name == "include" }.size() == 1 + + and: + def newSchema = GraphQLSchema.newSchema(schema).build() + then: + newSchema.getDirectives().findAll { it.name == "skip" }.size() == 1 + newSchema.getDirectives().findAll { it.name == "include" }.size() == 1 + } } diff --git a/src/test/groovy/graphql/schema/idl/SchemaPrinterTest.groovy b/src/test/groovy/graphql/schema/idl/SchemaPrinterTest.groovy index 5f7f6ce409..e946a06278 100644 --- a/src/test/groovy/graphql/schema/idl/SchemaPrinterTest.groovy +++ b/src/test/groovy/graphql/schema/idl/SchemaPrinterTest.groovy @@ -5,9 +5,11 @@ import graphql.TestUtil import graphql.TypeResolutionEnvironment import graphql.introspection.IntrospectionQuery import graphql.introspection.IntrospectionResultToSchema +import graphql.language.IntValue +import graphql.language.StringValue import graphql.schema.Coercing +import graphql.schema.GraphQLAppliedDirective import graphql.schema.GraphQLCodeRegistry -import graphql.schema.GraphQLDirective import graphql.schema.GraphQLEnumType import graphql.schema.GraphQLEnumValueDefinition import graphql.schema.GraphQLFieldDefinition @@ -99,9 +101,20 @@ class SchemaPrinterTest extends Specification { } def "argsString"() { - def argument1 = newArgument().name("arg1").type(list(nonNull(GraphQLInt))).defaultValue(10).build() - def argument2 = newArgument().name("arg2").type(GraphQLString).build(); - def argument3 = newArgument().name("arg3").type(GraphQLString).defaultValue("default").build() + def argument1 = newArgument() + .name("arg1") + .type(list(nonNull(GraphQLInt))) + .defaultValueLiteral(IntValue.newIntValue().value(10).build()) + .build() + def argument2 = newArgument() + .name("arg2") + .type(GraphQLString) + .build() + def argument3 = newArgument() + .name("arg3") + .type(GraphQLString) + .defaultValueLiteral(StringValue.newStringValue().value("default").build()) + .build() def argStr = new SchemaPrinter().argsString([argument1, argument2, argument3]) expect: @@ -110,9 +123,20 @@ class SchemaPrinterTest extends Specification { } def "argsString_sorts"() { - def argument1 = newArgument().name("arg1").type(list(nonNull(GraphQLInt))).defaultValue(10).build() - def argument2 = newArgument().name("arg2").type(GraphQLString).build(); - def argument3 = newArgument().name("arg3").type(GraphQLString).defaultValue("default").build() + def argument1 = newArgument() + .name("arg1") + .type(list(nonNull(GraphQLInt))) + .defaultValueLiteral(IntValue.newIntValue().value(10).build()) + .build() + def argument2 = newArgument() + .name("arg2") + .type(GraphQLString) + .build() + def argument3 = newArgument() + .name("arg3") + .type(GraphQLString) + .defaultValueLiteral(StringValue.newStringValue().value("default").build()) + .build() def argStr = new SchemaPrinter().argsString([argument2, argument1, argument3]) expect: @@ -121,8 +145,18 @@ class SchemaPrinterTest extends Specification { } def "argsString_comments"() { - def argument1 = newArgument().name("arg1").description("A multiline\ncomment").type(list(nonNull(GraphQLInt))).defaultValue(10).build() - def argument2 = newArgument().name("arg2").description("A single line comment").type(list(nonNull(GraphQLInt))).defaultValue(10).build() + def argument1 = newArgument() + .name("arg1") + .description("A multiline\ncomment") + .type(list(nonNull(GraphQLInt))) + .defaultValueLiteral(IntValue.newIntValue().value(10).build()) + .build() + def argument2 = newArgument() + .name("arg2") + .description("A single line comment") + .type(list(nonNull(GraphQLInt))) + .defaultValueLiteral(IntValue.newIntValue().value(10).build()) + .build() def argStr = new SchemaPrinter().argsString([argument1, argument2]) expect: @@ -394,12 +428,19 @@ enum Enum { .name("Union") .description("About union") .possibleType(possibleType) - .typeResolver({ it -> null }) .build() GraphQLFieldDefinition fieldDefinition2 = newFieldDefinition() .name("field").type(unionType).build() + + def codeRegistry = GraphQLCodeRegistry.newCodeRegistry() + .typeResolver(unionType, { it -> null }) + .build() def queryType = newObject().name("Query").field(fieldDefinition2).build() - def schema = GraphQLSchema.newSchema().query(queryType).build() + def schema = GraphQLSchema.newSchema() + .codeRegistry(codeRegistry) + .query(queryType) + .build() + when: def result = new SchemaPrinter(noDirectivesOption).print(schema) @@ -429,12 +470,19 @@ type Query { .name("Union") .possibleType(possibleType1) .possibleType(possibleType2) - .typeResolver({ it -> null }) .build() GraphQLFieldDefinition fieldDefinition2 = newFieldDefinition() .name("field").type(unionType).build() + + def codeRegistry = GraphQLCodeRegistry.newCodeRegistry() + .typeResolver(unionType, { it -> null }) + .build() def queryType = newObject().name("Query").field(fieldDefinition2).build() - def schema = GraphQLSchema.newSchema().query(queryType).build() + def schema = GraphQLSchema.newSchema() + .codeRegistry(codeRegistry) + .query(queryType) + .build() + when: def result = new SchemaPrinter(noDirectivesOption).print(schema) @@ -492,12 +540,19 @@ input Input { .name("Interface") .description("about interface") .field(newFieldDefinition().name("field").description("about field").type(GraphQLString).build()) - .typeResolver({ it -> null }) .build() GraphQLFieldDefinition fieldDefinition = newFieldDefinition() .name("field").type(graphQLInterfaceType).build() + + def codeRegistry = GraphQLCodeRegistry.newCodeRegistry() + .typeResolver(graphQLInterfaceType, { it -> null }) + .build() def queryType = newObject().name("Query").field(fieldDefinition).build() - def schema = GraphQLSchema.newSchema().query(queryType).build() + def schema = GraphQLSchema.newSchema() + .codeRegistry(codeRegistry) + .query(queryType) + .build() + when: def result = new SchemaPrinter(noDirectivesOption).print(schema) @@ -818,7 +873,6 @@ type Query { ''' } - def idlWithDirectives() { return """ directive @interfaceFieldDirective on FIELD_DEFINITION @@ -1992,14 +2046,14 @@ type Query { ''' when: - def newDirective = GraphQLDirective.newDirective().name("foo") + def newAppliedDirective = GraphQLAppliedDirective.newDirective().name("foo") .argument({ it.name("arg").type(compoundType).valueProgrammatic(["a": "A", "b": "B"]) }) .build() objType = newObject().name("obj").field({ - it.name("f").type(GraphQLString).withDirective(newDirective) + it.name("f").type(GraphQLString).withAppliedDirective(newAppliedDirective) }).build() result = new SchemaPrinter().print(objType) @@ -2012,6 +2066,28 @@ type Query { ''' } + def "directive containing formatting specifiers"() { + def constraintAppliedDirective = GraphQLAppliedDirective.newDirective().name("constraint") + .argument({ + it.name("regex").type(GraphQLString).valueProgrammatic("%") + }) + .build() + + GraphQLInputObjectType type = GraphQLInputObjectType.newInputObject().name("Person") + .field({ it.name("thisMustBeAPercentageSign").type(GraphQLString).withAppliedDirective(constraintAppliedDirective) }) + .build() + + when: + def result = new SchemaPrinter().print(type) + + + then: + result == '''input Person { + thisMustBeAPercentageSign: String @constraint(regex : "%") +} +''' + } + def "can specify a new ordering for the schema printer"() { def sdl = """ diff --git a/src/test/groovy/graphql/schema/idl/SchemaTypeCheckerTest.groovy b/src/test/groovy/graphql/schema/idl/SchemaTypeCheckerTest.groovy index 0ce9956d74..59e027073b 100644 --- a/src/test/groovy/graphql/schema/idl/SchemaTypeCheckerTest.groovy +++ b/src/test/groovy/graphql/schema/idl/SchemaTypeCheckerTest.groovy @@ -708,6 +708,35 @@ class SchemaTypeCheckerTest extends Specification { } + def "directives on arguments are not relevant"() { + def spec = """ + directive @d on ARGUMENT_DEFINITION + interface InterfaceType { + fieldB(arg1 : String = "defaultVal", arg2 : String @d, arg3 : Int @d) : String + } + + type BaseType { + fieldX : Int + } + + extend type BaseType implements InterfaceType { + fieldB(arg1 : String = "defaultVal" @d, arg2 : String, arg3 : Int) : String + } + + schema { + query : BaseType + } + """ + + def result = check(spec) + + expect: + result.isEmpty() + + } + + + def "test field arguments on object can contain additional optional arguments"() { def spec = """ interface InterfaceType { diff --git a/src/test/groovy/graphql/schema/idl/SchemaTypeDirectivesCheckerTest.groovy b/src/test/groovy/graphql/schema/idl/SchemaTypeDirectivesCheckerTest.groovy index 13c23cf883..887fe97f5c 100644 --- a/src/test/groovy/graphql/schema/idl/SchemaTypeDirectivesCheckerTest.groovy +++ b/src/test/groovy/graphql/schema/idl/SchemaTypeDirectivesCheckerTest.groovy @@ -3,12 +3,12 @@ package graphql.schema.idl import graphql.Scalars import graphql.schema.GraphQLScalarType import graphql.schema.idl.errors.DirectiveIllegalLocationError +import graphql.schema.idl.errors.DirectiveIllegalReferenceError import graphql.schema.idl.errors.DirectiveMissingNonNullArgumentError import graphql.schema.idl.errors.DirectiveUndeclaredError import graphql.schema.idl.errors.DirectiveUnknownArgumentError import graphql.schema.idl.errors.IllegalNameError import graphql.schema.idl.errors.NotAnInputTypeError -import graphql.schema.idl.errors.DirectiveIllegalReferenceError import spock.lang.Specification class SchemaTypeDirectivesCheckerTest extends Specification { @@ -27,6 +27,8 @@ class SchemaTypeDirectivesCheckerTest extends Specification { ENUM | ENUM_VALUE | INPUT_OBJECT | INPUT_FIELD_DEFINITION + directive @d(arg: String @testDirective) on FIELD + type ObjectType @testDirective(knownArg : "x") { field(arg1 : String @testDirective(knownArg : "x")) : String @testDirective(knownArg : "x") diff --git a/src/test/groovy/graphql/schema/idl/WiringFactoryTest.groovy b/src/test/groovy/graphql/schema/idl/WiringFactoryTest.groovy index fab6cde1dc..edb7efc12d 100644 --- a/src/test/groovy/graphql/schema/idl/WiringFactoryTest.groovy +++ b/src/test/groovy/graphql/schema/idl/WiringFactoryTest.groovy @@ -305,45 +305,6 @@ class WiringFactoryTest extends Specification { wiringFactory.fields == ["id", "homePlanet"] } - def "@fetch directive is respected by default data fetcher wiring if added"() { - def spec = """ - - directive @fetch(from : String!) on FIELD_DEFINITION - - type Query { - name : String, - homePlanet: String @fetch(from : "planetOfBirth") - } - """ - - def wiringFactory = new WiringFactory() { - } - def wiring = RuntimeWiring.newRuntimeWiring() - .wiringFactory(wiringFactory) - .directiveWiring(new FetchSchemaDirectiveWiring()) - .build() - - def schema = TestUtil.schema(spec, wiring) - - GraphQLObjectType type = schema.getType("Query") as GraphQLObjectType - - expect: - def fetcher = schema.getCodeRegistry().getDataFetcher(type, type.getFieldDefinition("homePlanet")) - fetcher instanceof PropertyDataFetcher - - PropertyDataFetcher propertyDataFetcher = fetcher as PropertyDataFetcher - propertyDataFetcher.getPropertyName() == "planetOfBirth" - // - // no directive - plain name - // - def fetcher2 = schema.getCodeRegistry().getDataFetcher(type, type.getFieldDefinition("name")) - fetcher2 instanceof PropertyDataFetcher - - PropertyDataFetcher propertyDataFetcher2 = fetcher2 as PropertyDataFetcher - propertyDataFetcher2.getPropertyName() == "name" - - } - def "Name"() { WiringFactory wf = new WiringFactory() { @Override diff --git a/src/test/groovy/graphql/schema/impl/SchemaUtilTest.groovy b/src/test/groovy/graphql/schema/impl/SchemaUtilTest.groovy index 5b39a84aeb..19dc803bfa 100644 --- a/src/test/groovy/graphql/schema/impl/SchemaUtilTest.groovy +++ b/src/test/groovy/graphql/schema/impl/SchemaUtilTest.groovy @@ -11,7 +11,6 @@ import graphql.schema.GraphQLObjectType import graphql.schema.GraphQLType import graphql.schema.GraphQLTypeReference import graphql.schema.GraphQLUnionType -import graphql.schema.impl.SchemaUtil import spock.lang.Specification import static graphql.Scalars.GraphQLBoolean @@ -163,8 +162,23 @@ class SchemaUtilTest extends Specification { when: GraphQLUnionType pet = ((GraphQLUnionType) SchemaWithReferences.getType("Pet")) GraphQLObjectType person = ((GraphQLObjectType) SchemaWithReferences.getType("Person")) - GraphQLArgument cacheEnabled = DirectivesUtil.directiveWithArg( - SchemaWithReferences.getDirectives(), Cache.getName(), "enabled").get(); + GraphQLArgument cacheEnabled = SchemaWithReferences.getDirectivesByName() + .get(Cache.getName()).getArgument("enabled") + + then: + SchemaWithReferences.allTypesAsList.findIndexOf { it instanceof GraphQLTypeReference } == -1 + pet.types.findIndexOf { it instanceof GraphQLTypeReference } == -1 + person.interfaces.findIndexOf { it instanceof GraphQLTypeReference } == -1 + !(cacheEnabled.getType() instanceof GraphQLTypeReference) + } + + def "all references are replaced with deprecated directiveWithArg"() { + when: + GraphQLUnionType pet = ((GraphQLUnionType) SchemaWithReferences.getType("Pet")) + GraphQLObjectType person = ((GraphQLObjectType) SchemaWithReferences.getType("Person")) + GraphQLArgument cacheEnabled = DirectivesUtil.directiveWithArg( // Retain for test coverage + SchemaWithReferences.getDirectives(), Cache.getName(), "enabled").get() + then: SchemaWithReferences.allTypesAsList.findIndexOf { it instanceof GraphQLTypeReference } == -1 pet.types.findIndexOf { it instanceof GraphQLTypeReference } == -1 diff --git a/src/test/groovy/graphql/schema/somepackage/RecordLikeClass.java b/src/test/groovy/graphql/schema/somepackage/RecordLikeClass.java new file mode 100644 index 0000000000..57cc04f2f1 --- /dev/null +++ b/src/test/groovy/graphql/schema/somepackage/RecordLikeClass.java @@ -0,0 +1,34 @@ +package graphql.schema.somepackage; + +import graphql.schema.DataFetchingEnvironment; + +/** + * This is obviously not an actual record class from Java 14 onwards, but it + * smells like one and that's enough really. Its public, not derived from another + * class and has a public method named after a property + */ +public class RecordLikeClass { + + public String recordProperty() { + return "recordProperty"; + } + + public String recordArgumentMethod(DataFetchingEnvironment environment) { + return "recordArgumentMethod"; + } + + @Override + public int hashCode() { + return 666; + } + + @Override + public boolean equals(Object obj) { + return super.equals(obj); + } + + @Override + public String toString() { + return "toString"; + } +} diff --git a/src/test/groovy/graphql/schema/somepackage/RecordLikeTwoClassesDown.java b/src/test/groovy/graphql/schema/somepackage/RecordLikeTwoClassesDown.java new file mode 100644 index 0000000000..4e744f2872 --- /dev/null +++ b/src/test/groovy/graphql/schema/somepackage/RecordLikeTwoClassesDown.java @@ -0,0 +1,4 @@ +package graphql.schema.somepackage; + +public class RecordLikeTwoClassesDown extends RecordLikeClass { +} diff --git a/src/test/groovy/graphql/schema/transform/FieldVisibilitySchemaTransformationTest.groovy b/src/test/groovy/graphql/schema/transform/FieldVisibilitySchemaTransformationTest.groovy index c6f25fd367..00b7edd505 100644 --- a/src/test/groovy/graphql/schema/transform/FieldVisibilitySchemaTransformationTest.groovy +++ b/src/test/groovy/graphql/schema/transform/FieldVisibilitySchemaTransformationTest.groovy @@ -2,7 +2,8 @@ package graphql.schema.transform import graphql.Scalars import graphql.TestUtil -import graphql.introspection.Introspection +import graphql.schema.GraphQLAppliedDirective +import graphql.schema.GraphQLCodeRegistry import graphql.schema.GraphQLDirectiveContainer import graphql.schema.GraphQLInputObjectType import graphql.schema.GraphQLObjectType @@ -11,7 +12,6 @@ import graphql.schema.TypeResolver import graphql.schema.idl.SchemaPrinter import spock.lang.Specification -import static graphql.schema.GraphQLDirective.newDirective import static graphql.schema.GraphQLFieldDefinition.newFieldDefinition import static graphql.schema.GraphQLInterfaceType.newInterface import static graphql.schema.GraphQLObjectType.newObject @@ -20,7 +20,7 @@ import static graphql.schema.GraphQLTypeReference.typeRef class FieldVisibilitySchemaTransformationTest extends Specification { def visibilitySchemaTransformation = new FieldVisibilitySchemaTransformation({ environment -> - def directives = (environment.schemaElement as GraphQLDirectiveContainer).directives + def directives = (environment.schemaElement as GraphQLDirectiveContainer).appliedDirectives return directives.find({ directive -> directive.name == "private" }) == null }) @@ -941,11 +941,11 @@ class FieldVisibilitySchemaTransformationTest extends Specification { .field(newFieldDefinition().name("account").type(typeRef("Account")).build()) .build() - def privateDirective = newDirective().name("private").build() + def privateDirective = GraphQLAppliedDirective.newDirective().name("private").build() def account = newObject() .name("Account") .field(newFieldDefinition().name("name").type(Scalars.GraphQLString).build()) - .field(newFieldDefinition().name("billingStatus").type(typeRef("SuperSecretCustomerData")).withDirective(privateDirective).build()) + .field(newFieldDefinition().name("billingStatus").type(typeRef("SuperSecretCustomerData")).withAppliedDirective(privateDirective).build()) .build() def billingStatus = newObject() @@ -957,16 +957,19 @@ class FieldVisibilitySchemaTransformationTest extends Specification { def secretData = newInterface() .name("SuperSecretCustomerData") .field(newFieldDefinition().name("id").type(Scalars.GraphQLString).build()) - .typeResolver(Mock(TypeResolver)) + .build() + + def codeRegistry = GraphQLCodeRegistry.newCodeRegistry() + .typeResolver(secretData, Mock(TypeResolver)) .build() def schema = GraphQLSchema.newSchema() + .codeRegistry(codeRegistry) .query(query) .additionalType(billingStatus) .additionalType(account) .additionalType(billingStatus) .additionalType(secretData) - .additionalDirective(privateDirective) .build() when: @@ -987,11 +990,11 @@ class FieldVisibilitySchemaTransformationTest extends Specification { .field(newFieldDefinition().name("account").type(typeRef("Account")).build()) .build() - def privateDirective = newDirective().name("private").build() + def privateDirective = GraphQLAppliedDirective.newDirective().name("private").build() def account = newObject() .name("Account") .field(newFieldDefinition().name("name").type(Scalars.GraphQLString).build()) - .field(newFieldDefinition().name("billingStatus").type(typeRef("BillingStatus")).withDirective(privateDirective).build()) + .field(newFieldDefinition().name("billingStatus").type(typeRef("BillingStatus")).withAppliedDirective(privateDirective).build()) .build() def billingStatus = newObject() @@ -1003,16 +1006,19 @@ class FieldVisibilitySchemaTransformationTest extends Specification { def secretData = newInterface() .name("SuperSecretCustomerData") .field(newFieldDefinition().name("id").type(Scalars.GraphQLString).build()) - .typeResolver(Mock(TypeResolver)) + .build() + + def codeRegistry = GraphQLCodeRegistry.newCodeRegistry() + .typeResolver(secretData, Mock(TypeResolver)) .build() def schema = GraphQLSchema.newSchema() + .codeRegistry(codeRegistry) .query(query) .additionalType(billingStatus) .additionalType(account) .additionalType(billingStatus) .additionalType(secretData) - .additionalDirective(privateDirective) .build() when: @@ -1032,11 +1038,11 @@ class FieldVisibilitySchemaTransformationTest extends Specification { .field(newFieldDefinition().name("account").type(typeRef("Account")).build()) .build() - def privateDirective = newDirective().name("private").build() + def privateDirective = GraphQLAppliedDirective.newDirective().name("private").build() def account = newObject() .name("Account") .field(newFieldDefinition().name("name").type(Scalars.GraphQLString).build()) - .field(newFieldDefinition().name("billingStatus").type(typeRef("BillingStatus")).withDirective(privateDirective).build()) + .field(newFieldDefinition().name("billingStatus").type(typeRef("BillingStatus")).withAppliedDirective(privateDirective).build()) .build() def billingStatus = newObject() @@ -1048,7 +1054,6 @@ class FieldVisibilitySchemaTransformationTest extends Specification { .query(query) .additionalType(billingStatus) .additionalType(account) - .additionalDirective(privateDirective) .build() when: @@ -1066,7 +1071,7 @@ class FieldVisibilitySchemaTransformationTest extends Specification { def callbacks = [] def visibilitySchemaTransformation = new FieldVisibilitySchemaTransformation({ environment -> - def directives = (environment.schemaElement as GraphQLDirectiveContainer).directives + def directives = (environment.schemaElement as GraphQLDirectiveContainer).appliedDirectives return directives.find({ directive -> directive.name == "private" }) == null }, { -> callbacks << "before" }, { -> callbacks << "after"} ) @@ -1178,4 +1183,67 @@ class FieldVisibilitySchemaTransformationTest extends Specification { (restrictedSchema.getType("Account") as GraphQLObjectType).getFieldDefinition("billingStatus") == null restrictedSchema.getType("BillingStatus") != null } + + def "can remove a field with a directive containing enum argument"() { + given: + GraphQLSchema schema = TestUtil.schema(""" + + directive @private(privateType: SecretType) on FIELD_DEFINITION + enum SecretType { + SUPER_SECRET + NOT_SO_SECRET + } + + type Query { + account: Account + } + + type Account { + name: String + billingStatus: BillingStatus @private(privateType: NOT_SO_SECRET) + } + + type BillingStatus { + accountNumber: String + } + """) + + when: + GraphQLSchema restrictedSchema = visibilitySchemaTransformation.apply(schema) + + then: + (restrictedSchema.getType("Account") as GraphQLObjectType).getFieldDefinition("billingStatus") == null + restrictedSchema.getType("BillingStatus") == null + } + + def "can remove a field with a directive containing type argument"() { + given: + GraphQLSchema schema = TestUtil.schema(""" + + directive @private(privateType: SecretType) on FIELD_DEFINITION + input SecretType { + description: String + } + + type Query { + account: Account + } + + type Account { + name: String + billingStatus: BillingStatus @private(privateType: { description: "secret" }) + } + + type BillingStatus { + accountNumber: String + } + """) + + when: + GraphQLSchema restrictedSchema = visibilitySchemaTransformation.apply(schema) + + then: + (restrictedSchema.getType("Account") as GraphQLObjectType).getFieldDefinition("billingStatus") == null + restrictedSchema.getType("BillingStatus") == null + } } diff --git a/src/test/groovy/graphql/schema/usage/SchemaUsageSupportTest.groovy b/src/test/groovy/graphql/schema/usage/SchemaUsageSupportTest.groovy index 091ddea02d..f0cff6c7fc 100644 --- a/src/test/groovy/graphql/schema/usage/SchemaUsageSupportTest.groovy +++ b/src/test/groovy/graphql/schema/usage/SchemaUsageSupportTest.groovy @@ -16,6 +16,7 @@ class SchemaUsageSupportTest extends Specification { f3 : RefUnion1 f4 : RefEnum1 f5 : String + f6 : RefUnion2 f_arg1( arg : RefInput1) : String f_arg2( arg : [RefInput2]) : String @@ -53,6 +54,12 @@ class SchemaUsageSupportTest extends Specification { union RefUnion1 = Ref1 | Ref2 + + type RefByUnionOnly1 { f : ID} + type RefByUnionOnly2 { f : ID} + + union RefUnion2 = RefByUnionOnly1 | RefByUnionOnly2 + enum RefEnum1 { A, B } @@ -154,6 +161,10 @@ class SchemaUsageSupportTest extends Specification { schemaUsage.isStronglyReferenced(schema, "RefUnion1") + schemaUsage.isStronglyReferenced(schema, "RefUnion2") + schemaUsage.isStronglyReferenced(schema, "RefByUnionOnly1") + schemaUsage.isStronglyReferenced(schema, "RefByUnionOnly2") + schemaUsage.isStronglyReferenced(schema, "RefInput1") schemaUsage.isStronglyReferenced(schema, "RefInput2") schemaUsage.isStronglyReferenced(schema, "RefInput3") diff --git a/src/test/groovy/graphql/schema/validation/TypeAndFieldRuleTest.groovy b/src/test/groovy/graphql/schema/validation/TypeAndFieldRuleTest.groovy index c516fcdcfb..8c3f4fc2a3 100644 --- a/src/test/groovy/graphql/schema/validation/TypeAndFieldRuleTest.groovy +++ b/src/test/groovy/graphql/schema/validation/TypeAndFieldRuleTest.groovy @@ -1,6 +1,7 @@ package graphql.schema.validation import graphql.TestUtil +import graphql.schema.GraphQLCodeRegistry import graphql.schema.GraphQLObjectType import graphql.schema.GraphQLTypeReference import graphql.schema.TypeResolverProxy @@ -10,7 +11,6 @@ import static graphql.schema.GraphQLUnionType.newUnionType class TypeAndFieldRuleTest extends Specification { - def "type must define one or more fields."() { when: def sdl = ''' @@ -55,7 +55,6 @@ class TypeAndFieldRuleTest extends Specification { e.message == "invalid schema:\n\"InputType\" must define one or more fields." } - def "field name must not begin with \"__\""() { when: def sdl = ''' @@ -93,8 +92,6 @@ class TypeAndFieldRuleTest extends Specification { e.message == "invalid schema:\n\"Interface\" must define one or more fields." } - - def "union member types must be object types"() { def sdl = ''' type Query { dummy: String } @@ -108,7 +105,10 @@ class TypeAndFieldRuleTest extends Specification { } ''' when: - def graphQLSchema = TestUtil.schema(sdl) + def codeRegistry = GraphQLCodeRegistry.newCodeRegistry() + .typeResolver("Interface", { null }) + .build() + def graphQLSchema = TestUtil.schema(sdl).transform({it.codeRegistry(codeRegistry)}) // this is a little convoluted, since this rule is repeated in the schemaChecker // we add the invalid union after schema creation so we can cover the validation from @@ -116,10 +116,11 @@ class TypeAndFieldRuleTest extends Specification { def unionType = newUnionType().name("unionWithNonObjectTypes") .possibleType(graphQLSchema.getObjectType("Object")) .possibleType(GraphQLTypeReference.typeRef("Interface")) - .typeResolver(new TypeResolverProxy()) .build() - - graphQLSchema.transform({ schema -> schema.additionalType(unionType) }) + codeRegistry = codeRegistry.transform({it.typeResolver(unionType, new TypeResolverProxy())}) + graphQLSchema.transform({ schema -> schema + .additionalType(unionType) + .codeRegistry(codeRegistry)}) then: InvalidSchemaException e = thrown(InvalidSchemaException) @@ -149,10 +150,15 @@ class TypeAndFieldRuleTest extends Specification { def unionType = newUnionType().name("unionWithNonObjectTypes") .possibleType(graphQLSchema.getObjectType("Object")) .possibleType(stubObjectType) - .typeResolver(new TypeResolverProxy()) .build() - graphQLSchema.transform({ schema -> schema.additionalType(unionType) }) + def codeRegistry = GraphQLCodeRegistry.newCodeRegistry() + .typeResolver(unionType, new TypeResolverProxy()) + .build() + + graphQLSchema.transform({ schema -> schema + .additionalType(unionType) + .codeRegistry(codeRegistry) }) then: InvalidSchemaException e = thrown(InvalidSchemaException) diff --git a/src/test/groovy/graphql/schema/validation/TypesImplementInterfacesTest.groovy b/src/test/groovy/graphql/schema/validation/TypesImplementInterfacesTest.groovy index d0d61fa4ae..c504393cf2 100644 --- a/src/test/groovy/graphql/schema/validation/TypesImplementInterfacesTest.groovy +++ b/src/test/groovy/graphql/schema/validation/TypesImplementInterfacesTest.groovy @@ -1,9 +1,7 @@ package graphql.schema.validation -import graphql.TypeResolutionEnvironment import graphql.schema.GraphQLInterfaceType import graphql.schema.GraphQLObjectType -import graphql.schema.TypeResolver import spock.lang.Specification import static SchemaValidationErrorType.ObjectDoesNotImplementItsInterfaces @@ -20,13 +18,6 @@ import static graphql.schema.GraphQLUnionType.newUnionType class TypesImplementInterfacesTest extends Specification { - TypeResolver typeResolver = new TypeResolver() { - @Override - GraphQLObjectType getType(TypeResolutionEnvironment env) { - null - } - } - GraphQLInterfaceType InterfaceType = newInterface() .name("Interface") @@ -39,7 +30,7 @@ class TypesImplementInterfacesTest extends Specification { .argument(newArgument().name("arg1").type(GraphQLString)) .argument(newArgument().name("arg2").type(GraphQLInt)) .argument(newArgument().name("arg3").type(GraphQLBoolean)) - .argument(newArgument().name("arg4").type(GraphQLString).defaultValue("ABC")) + .argument(newArgument().name("arg4").type(GraphQLString).defaultValueProgrammatic("ABC")) ) .field(newFieldDefinition().name("argField2").type(GraphQLString) @@ -47,14 +38,13 @@ class TypesImplementInterfacesTest extends Specification { .argument(newArgument().name("arg2").type(GraphQLInt)) .argument(newArgument().name("arg3").type(GraphQLBoolean)) ) - .typeResolver(typeResolver) .build() def "objects implement interfaces"() { given: SchemaValidationErrorCollector errorCollector = new SchemaValidationErrorCollector() - GraphQLObjectType objType = GraphQLObjectType.newObject() + GraphQLObjectType objType = newObject() .name("obj") .withInterface(InterfaceType) .field(newFieldDefinition().name("name").type(GraphQLString)) @@ -66,7 +56,7 @@ class TypesImplementInterfacesTest extends Specification { .argument(newArgument().name("arg1").type(GraphQLInt)) .argument(newArgument().name("arg2").type(GraphQLInt)) .argument(newArgument().name("arg3").type(GraphQLInt)) - .argument(newArgument().name("arg4").type(GraphQLString).defaultValue("XYZ")) + .argument(newArgument().name("arg4").type(GraphQLString).defaultValueProgrammatic("XYZ")) ) .field(newFieldDefinition().name("argField2").type(GraphQLString) @@ -102,7 +92,6 @@ class TypesImplementInterfacesTest extends Specification { def person = newInterface() .name("Person") .field(newFieldDefinition().name("name").type(GraphQLString).build()) - .typeResolver({}) .build() def actor = newObject() @@ -119,7 +108,6 @@ class TypesImplementInterfacesTest extends Specification { GraphQLInterfaceType interfaceType = newInterface() .name("TestInterface") .field(newFieldDefinition().name("field").type(person).build()) - .typeResolver({}) .build() GraphQLObjectType goodImpl = newObject() @@ -151,7 +139,6 @@ class TypesImplementInterfacesTest extends Specification { def person = newInterface() .name("Person") .field(newFieldDefinition().name("name").type(GraphQLString).build()) - .typeResolver({}) .build() def actor = newObject() @@ -168,7 +155,6 @@ class TypesImplementInterfacesTest extends Specification { GraphQLInterfaceType interfaceType = newInterface() .name("TestInterface") .field(newFieldDefinition().name("field").type(list(person)).build()) - .typeResolver({}) .build() GraphQLObjectType goodImpl = newObject() @@ -200,7 +186,6 @@ class TypesImplementInterfacesTest extends Specification { def person = newInterface() .name("Person") .field(newFieldDefinition().name("name").type(GraphQLString).build()) - .typeResolver({}) .build() def actor = newInterface() @@ -217,7 +202,6 @@ class TypesImplementInterfacesTest extends Specification { GraphQLInterfaceType interfaceType = newInterface() .name("TestInterface") .field(newFieldDefinition().name("field").type(list(person)).build()) - .typeResolver({}) .build() GraphQLObjectType goodImpl = newObject() @@ -260,7 +244,6 @@ class TypesImplementInterfacesTest extends Specification { .name("Person") .possibleType(actor) .possibleType(director) - .typeResolver({}) .build() def prop = newObject() @@ -271,7 +254,6 @@ class TypesImplementInterfacesTest extends Specification { GraphQLInterfaceType interfaceType = newInterface() .name("TestInterface") .field(newFieldDefinition().name("field").type(person).build()) - .typeResolver({}) .build() GraphQLObjectType goodImpl = newObject() @@ -303,7 +285,6 @@ class TypesImplementInterfacesTest extends Specification { GraphQLInterfaceType interfaceType = newInterface() .name("TestInterface") .field(newFieldDefinition().name("field").type(GraphQLString).build()) - .typeResolver({}) .build() GraphQLObjectType goodImpl = newObject() @@ -336,7 +317,6 @@ class TypesImplementInterfacesTest extends Specification { GraphQLInterfaceType memberInterface = newInterface() .name("TestMemberInterface") .field(newFieldDefinition().name("field").type(GraphQLString).build()) - .typeResolver({}) .build() GraphQLObjectType memberInterfaceImpl = newObject() @@ -348,7 +328,6 @@ class TypesImplementInterfacesTest extends Specification { GraphQLInterfaceType testInterface = newInterface() .name("TestInterface") .field(newFieldDefinition().name("field").type(nonNull(memberInterface)).build()) - .typeResolver({}) .build() GraphQLObjectType testInterfaceImpl = newObject() @@ -376,7 +355,7 @@ class TypesImplementInterfacesTest extends Specification { SchemaValidationErrorCollector errorCollector = new SchemaValidationErrorCollector() - GraphQLObjectType objType = GraphQLObjectType.newObject() + GraphQLObjectType objType = newObject() .name("Object") .withInterface(InterfaceType) .field(newFieldDefinition().name("argField").type(GraphQLString) @@ -405,7 +384,7 @@ class TypesImplementInterfacesTest extends Specification { SchemaValidationErrorCollector errorCollector = new SchemaValidationErrorCollector() - GraphQLObjectType objType = GraphQLObjectType.newObject() + GraphQLObjectType objType = newObject() .name("Object") .withInterface(InterfaceType) .field(newFieldDefinition().name("argField").type(GraphQLString) @@ -433,7 +412,7 @@ class TypesImplementInterfacesTest extends Specification { SchemaValidationErrorCollector errorCollector = new SchemaValidationErrorCollector() - GraphQLObjectType objType = GraphQLObjectType.newObject() + GraphQLObjectType objType = newObject() .name("Object") .withInterface(InterfaceType) .field(newFieldDefinition().name("argField").type(GraphQLString) @@ -461,7 +440,7 @@ class TypesImplementInterfacesTest extends Specification { SchemaValidationErrorCollector errorCollector = new SchemaValidationErrorCollector() - GraphQLObjectType objType = GraphQLObjectType.newObject() + GraphQLObjectType objType = newObject() .name("Object") .withInterface(InterfaceType) .field(newFieldDefinition().name("argField2").type(GraphQLString)) diff --git a/src/test/groovy/graphql/schema/visibility/GraphqlFieldVisibilityTest.groovy b/src/test/groovy/graphql/schema/visibility/GraphqlFieldVisibilityTest.groovy index b64495fc28..9f3e083977 100644 --- a/src/test/groovy/graphql/schema/visibility/GraphqlFieldVisibilityTest.groovy +++ b/src/test/groovy/graphql/schema/visibility/GraphqlFieldVisibilityTest.groovy @@ -6,6 +6,8 @@ import graphql.StarWarsSchema import graphql.execution.AsyncExecutionStrategy import graphql.introspection.IntrospectionQuery import graphql.language.Field +import graphql.schema.DataFetcher +import graphql.schema.FieldCoordinates import graphql.schema.GraphQLCodeRegistry import graphql.schema.GraphQLFieldDefinition import graphql.schema.GraphQLObjectType @@ -25,11 +27,11 @@ import static graphql.schema.visibility.NoIntrospectionGraphqlFieldVisibility.NO class GraphqlFieldVisibilityTest extends Specification { def "visibility is enforced"() { - GraphqlFieldVisibility banNameVisibility = newBlock().addPattern(".*\\.name").build() def schema = GraphQLSchema.newSchema() .query(StarWarsSchema.queryType) - .fieldVisibility(banNameVisibility) + .codeRegistry(StarWarsSchema.codeRegistry) + .fieldVisibility(banNameVisibility) // Retain deprecated builder for test coverage .build() def graphQL = GraphQL.newGraphQL(schema).build() @@ -56,13 +58,13 @@ class GraphqlFieldVisibilityTest extends Specification { } def "introspection visibility is enforced"() { - - given: - + GraphQLCodeRegistry codeRegistry = StarWarsSchema.codeRegistry.transform(builder -> { + builder.fieldVisibility(fieldVisibility) + }) def schema = GraphQLSchema.newSchema() .query(StarWarsSchema.queryType) - .fieldVisibility(fieldVisibility) + .codeRegistry(codeRegistry) .build() def graphQL = GraphQL.newGraphQL(schema).build() @@ -93,10 +95,12 @@ class GraphqlFieldVisibilityTest extends Specification { def "introspection turned off via field visibility"() { given: - + GraphQLCodeRegistry codeRegistry = StarWarsSchema.codeRegistry.transform(builder -> { + builder.fieldVisibility(NO_INTROSPECTION_FIELD_VISIBILITY) + }) def schema = GraphQLSchema.newSchema() .query(StarWarsSchema.queryType) - .fieldVisibility(NO_INTROSPECTION_FIELD_VISIBILITY) + .codeRegistry(codeRegistry) .build() def graphQL = GraphQL.newGraphQL(schema).build() @@ -115,7 +119,9 @@ class GraphqlFieldVisibilityTest extends Specification { def "schema printing filters on visibility"() { when: - def codeRegistry = GraphQLCodeRegistry.newCodeRegistry().fieldVisibility(DEFAULT_FIELD_VISIBILITY).build() + def codeRegistry = StarWarsSchema.codeRegistry.transform(builder -> { + builder.fieldVisibility(DEFAULT_FIELD_VISIBILITY) + }) def schema = GraphQLSchema.newSchema() .query(StarWarsSchema.queryType) .codeRegistry(codeRegistry) @@ -197,7 +203,9 @@ enum Episode { // and with specific bans when: - codeRegistry = GraphQLCodeRegistry.newCodeRegistry().fieldVisibility(ban(['Droid.id', 'Character.name', "QueryType.hero"])).build() + codeRegistry = StarWarsSchema.codeRegistry.transform(builder -> { + builder.fieldVisibility(ban(['Droid.id', 'Character.name', "QueryType.hero"])) + }) schema = GraphQLSchema.newSchema() .query(StarWarsSchema.queryType) .codeRegistry(codeRegistry) @@ -278,16 +286,15 @@ enum Episode { } def "ensure execution cant get to the field"() { - - when: + GraphQLCodeRegistry codeRegistry = GraphQLCodeRegistry.newCodeRegistry() + .fieldVisibility(ban(['Droid.appearsIn'])) + .build() def schema = GraphQLSchema.newSchema() .query(StarWarsSchema.queryType) - .fieldVisibility(ban(['Droid.appearsIn'])) + .codeRegistry(codeRegistry) .build() - - def executionStrategy = new AsyncExecutionStrategy() { // gives us access to this unit tested method @@ -310,17 +317,28 @@ enum Episode { .field(newInputObjectField().name("closedField").type(GraphQLString)) .build() - def inputQueryType = GraphQLObjectType.newObject().name("InputQuery") - .field(newFieldDefinition().name("hello").type(GraphQLString) - .argument(newArgument().name("arg").type(inputType)) - .dataFetcher({ env -> return "world" }) - ) - .build() + DataFetcher inputDataFetcher = { env -> return "world" } + + def inputQueryType = GraphQLObjectType.newObject() + .name("InputQuery") + .field(newFieldDefinition() + .name("hello") + .type(GraphQLString) + .argument(newArgument() + .name("arg") + .type(inputType)) + ).build() def "ensure input field are blocked"() { when: + def inputTypeCoordinates = FieldCoordinates.coordinates("InputQuery", "hello") + GraphQLCodeRegistry codeRegistry = GraphQLCodeRegistry.newCodeRegistry() + .dataFetcher(inputTypeCoordinates, inputDataFetcher) + .build() + def schema = GraphQLSchema.newSchema() + .codeRegistry(codeRegistry) .query(inputQueryType) .build() @@ -340,9 +358,10 @@ enum Episode { er.getData() == ["hello": "world"] when: + codeRegistry = codeRegistry.transform({builder -> builder.fieldVisibility(ban(['InputType.closedField']))}) schema = GraphQLSchema.newSchema() .query(inputQueryType) - .fieldVisibility(ban(['InputType.closedField'])) + .codeRegistry(codeRegistry) .build() graphQL = GraphQL.newGraphQL(schema).build() @@ -366,9 +385,12 @@ enum Episode { given: + GraphQLCodeRegistry codeRegistry = GraphQLCodeRegistry.newCodeRegistry() + .fieldVisibility(fieldVisibility) + .build() def schema = GraphQLSchema.newSchema() .query(inputQueryType) - .fieldVisibility(fieldVisibility) + .codeRegistry(codeRegistry) .build() def graphQL = GraphQL.newGraphQL(schema).build() diff --git a/src/test/groovy/graphql/validation/SpecValidation282Test.groovy b/src/test/groovy/graphql/validation/SpecValidation282Test.groovy index 92885e1c56..949f40cedc 100644 --- a/src/test/groovy/graphql/validation/SpecValidation282Test.groovy +++ b/src/test/groovy/graphql/validation/SpecValidation282Test.groovy @@ -2,7 +2,7 @@ package graphql.validation /** * validation examples used in the spec in given section - * http://facebook.github.io/graphql/#sec-Validation + * https://spec.graphql.org/October2021/#sec-Validation * * This test checks that an inline fragment containing just a directive * is parsed correctly diff --git a/src/test/groovy/graphql/validation/SpecValidation51Test.groovy b/src/test/groovy/graphql/validation/SpecValidation51Test.groovy index 78cda9ab7f..ef05fa8063 100644 --- a/src/test/groovy/graphql/validation/SpecValidation51Test.groovy +++ b/src/test/groovy/graphql/validation/SpecValidation51Test.groovy @@ -1,7 +1,7 @@ package graphql.validation /** * validation examples used in the spec in given section - * http://facebook.github.io/graphql/#sec-Validation + * https://spec.graphql.org/October2021/#sec-Validation * @author dwinsor * */ diff --git a/src/test/groovy/graphql/validation/SpecValidation562Test.groovy b/src/test/groovy/graphql/validation/SpecValidation562Test.groovy index bbec32c5d6..7a0f1ada73 100644 --- a/src/test/groovy/graphql/validation/SpecValidation562Test.groovy +++ b/src/test/groovy/graphql/validation/SpecValidation562Test.groovy @@ -1,7 +1,7 @@ package graphql.validation /** * validation examples used in the spec in given section - * http://facebook.github.io/graphql/#sec-Validation + * https://spec.graphql.org/October2021/#sec-Validation * @author dwinsor * */ diff --git a/src/test/groovy/graphql/validation/SpecValidation573Test.groovy b/src/test/groovy/graphql/validation/SpecValidation573Test.groovy index 01b400cc56..0d8565e227 100644 --- a/src/test/groovy/graphql/validation/SpecValidation573Test.groovy +++ b/src/test/groovy/graphql/validation/SpecValidation573Test.groovy @@ -1,7 +1,7 @@ package graphql.validation /** * validation examples used in the spec in given section - * http://facebook.github.io/graphql/#sec-Validation + * https://spec.graphql.org/October2021/#sec-Validation * */ class SpecValidation573Test extends SpecValidationBase { diff --git a/src/test/groovy/graphql/validation/SpecValidationBase.groovy b/src/test/groovy/graphql/validation/SpecValidationBase.groovy index 8d192c8148..1436f47524 100644 --- a/src/test/groovy/graphql/validation/SpecValidationBase.groovy +++ b/src/test/groovy/graphql/validation/SpecValidationBase.groovy @@ -5,7 +5,7 @@ import spock.lang.Specification /** * validation examples used in the spec - * http://facebook.github.io/graphql/#sec-Validation + * https://spec.graphql.org/October2021/#sec-Validation * @author dwinsor * */ diff --git a/src/test/groovy/graphql/validation/SpecValidationSchema.java b/src/test/groovy/graphql/validation/SpecValidationSchema.java index a59f502dbd..398dd1ad3e 100644 --- a/src/test/groovy/graphql/validation/SpecValidationSchema.java +++ b/src/test/groovy/graphql/validation/SpecValidationSchema.java @@ -1,8 +1,8 @@ package graphql.validation; import graphql.Scalars; -import graphql.TypeResolutionEnvironment; import graphql.schema.GraphQLArgument; +import graphql.schema.GraphQLCodeRegistry; import graphql.schema.GraphQLDirective; import graphql.schema.GraphQLEnumType; import graphql.schema.GraphQLInputObjectField; @@ -34,7 +34,7 @@ /** * Sample schema used in the spec for validation examples - * http://facebook.github.io/graphql/#sec-Validation + * https://spec.graphql.org/October2021/#sec-Validation * * @author dwinsor */ @@ -55,27 +55,6 @@ public class SpecValidationSchema { public static final GraphQLInterfaceType sentient = GraphQLInterfaceType.newInterface() .name("Sentient") .field(newFieldDefinition().name("name").type(nonNull(Scalars.GraphQLString))) - .typeResolver(new TypeResolver() { - @Override - public GraphQLObjectType getType(TypeResolutionEnvironment env) { - if (env.getObject() instanceof Human) return human; - if (env.getObject() instanceof Alien) return alien; - return null; - } - }) - .build(); - - public static final GraphQLInterfaceType pet = GraphQLInterfaceType.newInterface() - .name("Pet") - .field(newFieldDefinition().name("name").type(nonNull(Scalars.GraphQLString))) - .typeResolver(new TypeResolver() { - @Override - public GraphQLObjectType getType(TypeResolutionEnvironment env) { - if (env.getObject() instanceof Dog) return dog; - if (env.getObject() instanceof Cat) return cat; - return null; - } - }) .build(); public static final GraphQLObjectType human = GraphQLObjectType.newObject() @@ -91,6 +70,21 @@ public GraphQLObjectType getType(TypeResolutionEnvironment env) { .withInterface(SpecValidationSchema.sentient) .build(); + public static final TypeResolver sentientTypeResolver = env -> { + if (env.getObject() instanceof Human) { + return human; + } + if (env.getObject() instanceof Alien) { + return alien; + } + return null; + }; + + public static final GraphQLArgument catCommandArg = GraphQLArgument.newArgument() + .name("catCommand") + .type(nonNull(catCommand)) + .build(); + public static final GraphQLArgument dogCommandArg = GraphQLArgument.newArgument() .name("dogCommand") .type(nonNull(dogCommand)) @@ -101,9 +95,9 @@ public GraphQLObjectType getType(TypeResolutionEnvironment env) { .type(Scalars.GraphQLBoolean) .build(); - public static final GraphQLArgument catCommandArg = GraphQLArgument.newArgument() - .name("catCommand") - .type(nonNull(catCommand)) + public static final GraphQLInterfaceType pet = GraphQLInterfaceType.newInterface() + .name("Pet") + .field(newFieldDefinition().name("name").type(nonNull(Scalars.GraphQLString))) .build(); public static final GraphQLObjectType dog = GraphQLObjectType.newObject() @@ -112,9 +106,9 @@ public GraphQLObjectType getType(TypeResolutionEnvironment env) { .field(newFieldDefinition().name("nickname").type(Scalars.GraphQLString)) .field(newFieldDefinition().name("barkVolume").type(Scalars.GraphQLInt)) .field(newFieldDefinition().name("doesKnowCommand").type(nonNull(Scalars.GraphQLBoolean)) - .argument(singletonList(dogCommandArg))) + .arguments(singletonList(dogCommandArg))) .field(newFieldDefinition().name("isHousetrained").type(Scalars.GraphQLBoolean) - .argument(singletonList(atOtherHomesArg))) + .arguments(singletonList(atOtherHomesArg))) .field(newFieldDefinition().name("owner").type(human)) .withInterface(SpecValidationSchema.pet) .build(); @@ -125,40 +119,65 @@ public GraphQLObjectType getType(TypeResolutionEnvironment env) { .field(newFieldDefinition().name("nickname").type(Scalars.GraphQLString)) .field(newFieldDefinition().name("meowVolume").type(Scalars.GraphQLInt)) .field(newFieldDefinition().name("doesKnowCommand").type(nonNull(Scalars.GraphQLBoolean)) - .argument(singletonList(catCommandArg))) + .arguments(singletonList(catCommandArg))) .withInterface(SpecValidationSchema.pet) .build(); + public static final TypeResolver petTypeResolver = env -> { + if (env.getObject() instanceof Dog) { + return dog; + } + if (env.getObject() instanceof Cat) { + return cat; + } + return null; + }; + public static final GraphQLUnionType catOrDog = GraphQLUnionType.newUnionType() .name("CatOrDog") .possibleTypes(cat, dog) - .typeResolver(env -> { - if (env.getObject() instanceof Cat) return cat; - if (env.getObject() instanceof Dog) return dog; - return null; - }) .build(); + public static final TypeResolver catOrDogTypeResolver = env -> { + if (env.getObject() instanceof Cat) { + return cat; + } + if (env.getObject() instanceof Dog) { + return dog; + } + return null; + }; + public static final GraphQLUnionType dogOrHuman = GraphQLUnionType.newUnionType() .name("DogOrHuman") .possibleTypes(dog, human) - .typeResolver(env -> { - if (env.getObject() instanceof Human) return human; - if (env.getObject() instanceof Dog) return dog; - return null; - }) .build(); + public static final TypeResolver dogOrHumanTypeResolver = env -> { + if (env.getObject() instanceof Human) { + return human; + } + if (env.getObject() instanceof Dog) { + return dog; + } + return null; + }; + public static final GraphQLUnionType humanOrAlien = GraphQLUnionType.newUnionType() .name("HumanOrAlien") .possibleTypes(human, alien) - .typeResolver(env -> { - if (env.getObject() instanceof Human) return human; - if (env.getObject() instanceof Alien) return alien; - return null; - }) .build(); + public static final TypeResolver humanOrAlienTypeResolver = env -> { + if (env.getObject() instanceof Human) { + return human; + } + if (env.getObject() instanceof Alien) { + return alien; + } + return null; + }; + public static final GraphQLDirective dogDirective = GraphQLDirective.newDirective() .name("dogDirective") .argument(newArgument().name("arg1").type(GraphQLString).build()) @@ -180,7 +199,6 @@ public GraphQLObjectType getType(TypeResolutionEnvironment env) { .field(newFieldDefinition().name("cat").type(cat)) .build(); - @SuppressWarnings("serial") public static final Set specValidationDictionary = new HashSet() {{ add(dogCommand); add(catCommand); @@ -229,14 +247,24 @@ public GraphQLObjectType getType(TypeResolutionEnvironment env) { .validLocations(FIELD, FRAGMENT_SPREAD, FRAGMENT_DEFINITION, INLINE_FRAGMENT, QUERY) .build(); + public static final GraphQLCodeRegistry codeRegistry = GraphQLCodeRegistry.newCodeRegistry() + .typeResolver("Sentient", sentientTypeResolver) + .typeResolver("Pet", petTypeResolver) + .typeResolver("CatOrDog", catOrDogTypeResolver) + .typeResolver("DogOrHuman", dogOrHumanTypeResolver) + .typeResolver("HumanOrAlien", humanOrAlienTypeResolver) + .build(); + public static final GraphQLSchema specValidationSchema = GraphQLSchema.newSchema() .query(queryRoot) + .codeRegistry(codeRegistry) .subscription(subscriptionRoot) .additionalDirective(upperDirective) .additionalDirective(lowerDirective) .additionalDirective(dogDirective) .additionalDirective(nonNullDirective) .additionalDirective(objectArgumentDirective) - .build(specValidationDictionary); + .additionalTypes(specValidationDictionary) + .build(); } diff --git a/src/test/groovy/graphql/validation/SpecValidationSchemaPojos.java b/src/test/groovy/graphql/validation/SpecValidationSchemaPojos.java index 9b0346b3e1..da3d0c7622 100644 --- a/src/test/groovy/graphql/validation/SpecValidationSchemaPojos.java +++ b/src/test/groovy/graphql/validation/SpecValidationSchemaPojos.java @@ -2,7 +2,7 @@ /** * Sample schema pojos used in the spec for validation examples - * http://facebook.github.io/graphql/#sec-Validation + * https://spec.graphql.org/October2021/#sec-Validation * * @author dwinsor */ diff --git a/src/test/groovy/graphql/validation/ValidateCustomDirectives.groovy b/src/test/groovy/graphql/validation/ValidateCustomDirectives.groovy index f4f96470dc..5a1569e978 100644 --- a/src/test/groovy/graphql/validation/ValidateCustomDirectives.groovy +++ b/src/test/groovy/graphql/validation/ValidateCustomDirectives.groovy @@ -19,6 +19,7 @@ class ValidateCustomDirectives extends Specification { GraphQLSchema customDirectiveSchema = GraphQLSchema.newSchema() .query(SpecValidationSchema.queryRoot) + .codeRegistry(SpecValidationSchema.codeRegistry) .additionalDirective(SpecValidationSchema.dogDirective) .additionalDirective(customDirective) .additionalTypes(SpecValidationSchema.specValidationDictionary) diff --git a/src/test/groovy/graphql/validation/ValidationErrorToString.groovy b/src/test/groovy/graphql/validation/ValidationErrorToString.groovy new file mode 100644 index 0000000000..cfade59c28 --- /dev/null +++ b/src/test/groovy/graphql/validation/ValidationErrorToString.groovy @@ -0,0 +1,39 @@ +package graphql.validation + +import graphql.language.SourceLocation +import spock.lang.Specification + +class ValidationErrorToString extends Specification { + + def 'toString prints correctly ValidationError object when all fields are initialized'() { + given: + def sourceLocations = [new SourceLocation(5, 0), new SourceLocation(10, 1)] + def description = "Validation Error (UnknownType)" + def validationErrorClassification = ValidationErrorType.UnknownType + def queryPath = ["home", "address"] + def extensions = ["extension1": "first", "extension2": true, "extension3": 2] + + when: + def validationError = ValidationError + .newValidationError() + .sourceLocations(sourceLocations) + .description(description) + .validationErrorType(validationErrorClassification) + .queryPath(queryPath) + .extensions(extensions) + .build() + + then: + validationError.toString() == "ValidationError{validationErrorType=UnknownType, queryPath=[home, address], message=Validation Error (UnknownType), locations=[SourceLocation{line=5, column=0}, SourceLocation{line=10, column=1}], description='Validation Error (UnknownType)', extensions=[extension1=first, extension2=true, extension3=2]}" + } + + def 'toString prints correctly ValidationError object when all fields are empty'() { + when: + def validationError = ValidationError + .newValidationError() + .build() + + then: + validationError.toString() == "ValidationError{validationErrorType=null, queryPath=[], message=null, locations=[], description='null', extensions=[]}" + } +} diff --git a/src/test/groovy/graphql/validation/ValidationUtilTest.groovy b/src/test/groovy/graphql/validation/ValidationUtilTest.groovy index 8ed1d8e55c..3373809109 100644 --- a/src/test/groovy/graphql/validation/ValidationUtilTest.groovy +++ b/src/test/groovy/graphql/validation/ValidationUtilTest.groovy @@ -1,5 +1,6 @@ package graphql.validation +import graphql.GraphQLContext import graphql.StarWarsSchema import graphql.language.ArrayValue import graphql.language.BooleanValue @@ -25,8 +26,13 @@ import static graphql.schema.GraphQLNonNull.nonNull class ValidationUtilTest extends Specification { - def schema = GraphQLSchema.newSchema().query(StarWarsSchema.queryType).build() + def schema = GraphQLSchema.newSchema() + .query(StarWarsSchema.queryType) + .codeRegistry(StarWarsSchema.codeRegistry) + .build() def validationUtil = new ValidationUtil() + def graphQLContext = GraphQLContext.getDefault() + def locale = Locale.getDefault() def "getUnmodified type of list of nonNull"() { given: @@ -51,32 +57,32 @@ class ValidationUtilTest extends Specification { def "null and NonNull is invalid"() { expect: - !validationUtil.isValidLiteralValue(null, nonNull(GraphQLString),schema) + !validationUtil.isValidLiteralValue(null, nonNull(GraphQLString), schema, graphQLContext, locale) } def "NullValue and NonNull is invalid"() { expect: - !validationUtil.isValidLiteralValue(NullValue.newNullValue().build(), nonNull(GraphQLString),schema) + !validationUtil.isValidLiteralValue(NullValue.newNullValue().build(), nonNull(GraphQLString), schema, graphQLContext, locale) } def "a nonNull value for a NonNull type is valid"() { expect: - validationUtil.isValidLiteralValue(new StringValue("string"), nonNull(GraphQLString),schema) + validationUtil.isValidLiteralValue(new StringValue("string"), nonNull(GraphQLString), schema, graphQLContext, locale) } def "null is valid when type is NonNull"() { expect: - validationUtil.isValidLiteralValue(null, GraphQLString,schema) + validationUtil.isValidLiteralValue(null, GraphQLString, schema, graphQLContext, locale) } def "NullValue is valid when type is NonNull"() { expect: - validationUtil.isValidLiteralValue(NullValue.newNullValue().build(), GraphQLString,schema) + validationUtil.isValidLiteralValue(NullValue.newNullValue().build(), GraphQLString, schema, graphQLContext, locale) } def "variables are valid"() { expect: - validationUtil.isValidLiteralValue(new VariableReference("var"), GraphQLBoolean,schema) + validationUtil.isValidLiteralValue(new VariableReference("var"), GraphQLBoolean, schema, graphQLContext, locale) } def "ArrayValue and ListType is invalid when one entry is invalid"() { @@ -85,7 +91,7 @@ class ValidationUtilTest extends Specification { def type = list(GraphQLString) expect: - !validationUtil.isValidLiteralValue(arrayValue, type,schema) + !validationUtil.isValidLiteralValue(arrayValue, type,schema,graphQLContext, locale) } def "One value is a single element List"() { @@ -93,7 +99,7 @@ class ValidationUtilTest extends Specification { def singleValue = new BooleanValue(true) def type = list(GraphQLBoolean) expect: - validationUtil.isValidLiteralValue(singleValue, type,schema) + validationUtil.isValidLiteralValue(singleValue, type,schema,graphQLContext,locale) } def "a valid array"() { @@ -102,19 +108,19 @@ class ValidationUtilTest extends Specification { def type = list(GraphQLString) expect: - validationUtil.isValidLiteralValue(arrayValue, type,schema) + validationUtil.isValidLiteralValue(arrayValue, type,schema, graphQLContext,locale) } def "a valid scalar"() { given: expect: - validationUtil.isValidLiteralValue(new BooleanValue(true), GraphQLBoolean,schema) + validationUtil.isValidLiteralValue(new BooleanValue(true), GraphQLBoolean, schema, graphQLContext, locale) } def "invalid scalar"() { given: expect: - !validationUtil.isValidLiteralValue(new BooleanValue(true), GraphQLString,schema) + !validationUtil.isValidLiteralValue(new BooleanValue(true), GraphQLString, schema, graphQLContext, locale) } def "valid enum"() { @@ -122,21 +128,21 @@ class ValidationUtilTest extends Specification { def enumType = GraphQLEnumType.newEnum().name("enumType").value("PLUTO").build() expect: - validationUtil.isValidLiteralValue(new EnumValue("PLUTO"), enumType,schema) + validationUtil.isValidLiteralValue(new EnumValue("PLUTO"), enumType, schema, graphQLContext, locale) } def "invalid enum value"() { given: def enumType = GraphQLEnumType.newEnum().name("enumType").value("PLUTO").build() expect: - !validationUtil.isValidLiteralValue(new StringValue("MARS"), enumType,schema) + !validationUtil.isValidLiteralValue(new StringValue("MARS"), enumType, schema, graphQLContext, locale) } def "invalid enum name"() { given: def enumType = GraphQLEnumType.newEnum().name("enumType").value("PLUTO").build() expect: - !validationUtil.isValidLiteralValue(new EnumValue("MARS"), enumType,schema) + !validationUtil.isValidLiteralValue(new EnumValue("MARS"), enumType, schema, graphQLContext, locale) } def "a valid ObjectValue"() { @@ -151,7 +157,7 @@ class ValidationUtilTest extends Specification { objectValue.objectField(new ObjectField("hello", new StringValue("world"))) expect: - validationUtil.isValidLiteralValue(objectValue.build(), inputObjectType, schema) + validationUtil.isValidLiteralValue(objectValue.build(), inputObjectType, schema, graphQLContext, locale) } def "a invalid ObjectValue with a invalid field"() { @@ -166,7 +172,7 @@ class ValidationUtilTest extends Specification { objectValue.objectField(new ObjectField("hello", new BooleanValue(false))) expect: - !validationUtil.isValidLiteralValue(objectValue.build(), inputObjectType, schema) + !validationUtil.isValidLiteralValue(objectValue.build(), inputObjectType, schema, graphQLContext,locale) } def "a invalid ObjectValue with a missing field"() { @@ -180,7 +186,7 @@ class ValidationUtilTest extends Specification { def objectValue = ObjectValue.newObjectValue().build() expect: - !validationUtil.isValidLiteralValue(objectValue, inputObjectType,schema) + !validationUtil.isValidLiteralValue(objectValue, inputObjectType,schema, graphQLContext,locale) } def "a valid ObjectValue with a nonNull field and default value"() { @@ -190,11 +196,11 @@ class ValidationUtilTest extends Specification { .field(GraphQLInputObjectField.newInputObjectField() .name("hello") .type(nonNull(GraphQLString)) - .defaultValue("default")) + .defaultValueProgrammatic("default")) .build() def objectValue = ObjectValue.newObjectValue() expect: - validationUtil.isValidLiteralValue(objectValue.build(), inputObjectType, schema) + validationUtil.isValidLiteralValue(objectValue.build(), inputObjectType, schema, graphQLContext,locale) } } diff --git a/src/test/groovy/graphql/validation/rules/ArgumentsOfCorrectTypeTest.groovy b/src/test/groovy/graphql/validation/rules/ArgumentsOfCorrectTypeTest.groovy index b2b99db2bd..67c87407cf 100644 --- a/src/test/groovy/graphql/validation/rules/ArgumentsOfCorrectTypeTest.groovy +++ b/src/test/groovy/graphql/validation/rules/ArgumentsOfCorrectTypeTest.groovy @@ -1,5 +1,6 @@ package graphql.validation.rules +import graphql.GraphQLContext import graphql.language.Argument import graphql.language.ArrayValue import graphql.language.BooleanValue @@ -35,6 +36,8 @@ class ArgumentsOfCorrectTypeTest extends Specification { def setup() { argumentsOfCorrectType = new ArgumentsOfCorrectType(validationContext, errorCollector) + def context = GraphQLContext.getDefault() + validationContext.getGraphQLContext() >> context } def "valid type results in no error"() { @@ -77,7 +80,7 @@ class ArgumentsOfCorrectTypeTest extends Specification { !validationErrors.empty validationErrors.size() == 1 validationErrors.get(0).getValidationErrorType() == ValidationErrorType.WrongType - validationErrors.get(0).message == "Validation error (WrongType@[dog]) : argument 'arg1' with value 'IntValue{value=1}' is not a valid 'String' - Expected AST type 'StringValue' but was 'IntValue'." + validationErrors.get(0).message == "Validation error (WrongType@[dog]) : argument 'arg1' with value 'IntValue{value=1}' is not a valid 'String' - Expected an AST type of 'StringValue' but it was a 'IntValue'" } def "invalid input object type results in error"() { @@ -329,7 +332,7 @@ class ArgumentsOfCorrectTypeTest extends Specification { !validationErrors.empty validationErrors.size() == 1 validationErrors.get(0).getValidationErrorType() == ValidationErrorType.WrongType - validationErrors.get(0).message == "Validation error (WrongType@[dog/doesKnowCommand]) : argument 'dogCommand' with value 'EnumValue{name='PRETTY'}' is not a valid 'DogCommand' - Expected enum literal value not in allowable values - 'EnumValue{name='PRETTY'}'." + validationErrors.get(0).message == "Validation error (WrongType@[dog/doesKnowCommand]) : argument 'dogCommand' with value 'EnumValue{name='PRETTY'}' is not a valid 'DogCommand' - Literal value not in allowable values for enum 'DogCommand' - 'EnumValue{name='PRETTY'}'" } static List validate(String query) { diff --git a/src/test/groovy/graphql/validation/rules/Harness.java b/src/test/groovy/graphql/validation/rules/Harness.java index 6d751285c7..2b233b77d0 100644 --- a/src/test/groovy/graphql/validation/rules/Harness.java +++ b/src/test/groovy/graphql/validation/rules/Harness.java @@ -77,7 +77,7 @@ public class Harness { .argument(newArgument() .name("atOtherHomes") .type(GraphQLBoolean) - .defaultValue(true))) + .defaultValueProgrammatic(true))) .field(newFieldDefinition() .name("isAtLocation") .type(GraphQLBoolean) diff --git a/src/test/groovy/graphql/validation/rules/OverlappingFieldsCanBeMergedTest.groovy b/src/test/groovy/graphql/validation/rules/OverlappingFieldsCanBeMergedTest.groovy index deedfbed77..f9586b337f 100644 --- a/src/test/groovy/graphql/validation/rules/OverlappingFieldsCanBeMergedTest.groovy +++ b/src/test/groovy/graphql/validation/rules/OverlappingFieldsCanBeMergedTest.groovy @@ -1,14 +1,11 @@ package graphql.validation.rules - -import graphql.TypeResolutionEnvironment import graphql.i18n.I18n import graphql.language.Document import graphql.language.SourceLocation import graphql.parser.Parser -import graphql.schema.GraphQLObjectType +import graphql.schema.GraphQLCodeRegistry import graphql.schema.GraphQLSchema -import graphql.schema.TypeResolver import graphql.validation.LanguageTraversal import graphql.validation.RulesVisitor import graphql.validation.ValidationContext @@ -79,7 +76,7 @@ class OverlappingFieldsCanBeMergedTest extends Specification { errorCollector.getErrors()[0].locations == [new SourceLocation(3, 17), new SourceLocation(4, 17)] } - GraphQLSchema unionSchema() { + static GraphQLSchema unionSchema() { def StringBox = newObject().name("StringBox") .field(newFieldDefinition().name("scalar").type(GraphQLString)) .build() @@ -102,17 +99,18 @@ class OverlappingFieldsCanBeMergedTest extends Specification { def BoxUnion = newUnionType() .name("BoxUnion") .possibleTypes(StringBox, IntBox, NonNullStringBox1, NonNullStringBox2, ListStringBox1) - .typeResolver(new TypeResolver() { - @Override - GraphQLObjectType getType(TypeResolutionEnvironment env) { - return null - } - }) + .build() + def codeRegistry = GraphQLCodeRegistry.newCodeRegistry() + .typeResolver(BoxUnion, { env -> null }) .build() def QueryRoot = newObject() .name("QueryRoot") .field(newFieldDefinition().name("boxUnion").type(BoxUnion)).build() - return GraphQLSchema.newSchema().query(QueryRoot).build() + + return GraphQLSchema.newSchema() + .codeRegistry(codeRegistry) + .query(QueryRoot) + .build() } def 'conflicting scalar return types'() { diff --git a/src/test/groovy/graphql/validation/rules/ProvidedNonNullArgumentsTest.groovy b/src/test/groovy/graphql/validation/rules/ProvidedNonNullArgumentsTest.groovy index 8de2808b66..925f6603eb 100644 --- a/src/test/groovy/graphql/validation/rules/ProvidedNonNullArgumentsTest.groovy +++ b/src/test/groovy/graphql/validation/rules/ProvidedNonNullArgumentsTest.groovy @@ -68,7 +68,7 @@ class ProvidedNonNullArgumentsTest extends Specification { given: def fieldArg = GraphQLArgument.newArgument().name("arg") .type(GraphQLNonNull.nonNull(GraphQLString)) - .defaultValue("defaultVal") + .defaultValueProgrammatic("defaultVal") def fieldDef = GraphQLFieldDefinition.newFieldDefinition() .name("field") .type(GraphQLString) @@ -146,7 +146,7 @@ class ProvidedNonNullArgumentsTest extends Specification { given: def directiveArg = GraphQLArgument.newArgument() .name("arg").type(GraphQLNonNull.nonNull(GraphQLString)) - .defaultValue("defaultVal") + .defaultValueProgrammatic("defaultVal") def graphQLDirective = GraphQLDirective.newDirective() .name("directive") .argument(directiveArg) diff --git a/src/test/groovy/graphql/validation/rules/VariableDefaultValuesOfCorrectTypeTest.groovy b/src/test/groovy/graphql/validation/rules/VariableDefaultValuesOfCorrectTypeTest.groovy index cbf1a3a518..e69baff7b2 100644 --- a/src/test/groovy/graphql/validation/rules/VariableDefaultValuesOfCorrectTypeTest.groovy +++ b/src/test/groovy/graphql/validation/rules/VariableDefaultValuesOfCorrectTypeTest.groovy @@ -1,6 +1,8 @@ package graphql.validation.rules +import graphql.GraphQLContext import graphql.TestUtil +import graphql.i18n.I18n import graphql.language.BooleanValue import graphql.language.TypeName import graphql.language.VariableDefinition @@ -18,6 +20,11 @@ class VariableDefaultValuesOfCorrectTypeTest extends Specification { ValidationErrorCollector errorCollector = new ValidationErrorCollector() VariableDefaultValuesOfCorrectType defaultValuesOfCorrectType = new VariableDefaultValuesOfCorrectType(validationContext, errorCollector) + void setup() { + def context = GraphQLContext.getDefault() + validationContext.getGraphQLContext() >> context + } + def "default value has wrong type"() { given: BooleanValue defaultValue = BooleanValue.newBooleanValue(false).build() diff --git a/src/test/groovy/readme/ExecutionExamples.java b/src/test/groovy/readme/ExecutionExamples.java index ee3f3fa42a..0f8f01c7fe 100644 --- a/src/test/groovy/readme/ExecutionExamples.java +++ b/src/test/groovy/readme/ExecutionExamples.java @@ -15,6 +15,7 @@ import graphql.language.SourceLocation; import graphql.schema.DataFetcher; import graphql.schema.DataFetchingEnvironment; +import graphql.schema.GraphQLCodeRegistry; import graphql.schema.GraphQLFieldDefinition; import graphql.schema.GraphQLFieldsContainer; import graphql.schema.GraphQLSchema; @@ -164,21 +165,26 @@ private void blockedFields() { .addPattern("Droid.appearsIn") .addPattern(".*\\.hero") // it uses regular expressions .build(); + GraphQLCodeRegistry codeRegistry = GraphQLCodeRegistry.newCodeRegistry() + .fieldVisibility(blockedFields) + .build(); GraphQLSchema schema = GraphQLSchema.newSchema() .query(StarWarsSchema.queryType) - .fieldVisibility(blockedFields) + .codeRegistry(codeRegistry) .build(); - //::/FigureJ } private void noIntrospection() { //::FigureK + GraphQLCodeRegistry codeRegistry = GraphQLCodeRegistry.newCodeRegistry() + .fieldVisibility(NoIntrospectionGraphqlFieldVisibility.NO_INTROSPECTION_FIELD_VISIBILITY) + .build(); GraphQLSchema schema = GraphQLSchema.newSchema() .query(StarWarsSchema.queryType) - .fieldVisibility(NoIntrospectionGraphqlFieldVisibility.NO_INTROSPECTION_FIELD_VISIBILITY) + .codeRegistry(codeRegistry) .build(); //::/FigureK } diff --git a/src/test/groovy/readme/InstrumentationExamples.java b/src/test/groovy/readme/InstrumentationExamples.java index bae144b21e..3fa00e641d 100644 --- a/src/test/groovy/readme/InstrumentationExamples.java +++ b/src/test/groovy/readme/InstrumentationExamples.java @@ -8,8 +8,8 @@ import graphql.execution.instrumentation.Instrumentation; import graphql.execution.instrumentation.InstrumentationContext; import graphql.execution.instrumentation.InstrumentationState; -import graphql.execution.instrumentation.SimpleInstrumentation; import graphql.execution.instrumentation.SimpleInstrumentationContext; +import graphql.execution.instrumentation.SimplePerformantInstrumentation; import graphql.execution.instrumentation.fieldvalidation.FieldAndArguments; import graphql.execution.instrumentation.fieldvalidation.FieldValidation; import graphql.execution.instrumentation.fieldvalidation.FieldValidationEnvironment; @@ -20,6 +20,8 @@ import graphql.execution.instrumentation.tracing.TracingInstrumentation; import graphql.schema.DataFetcher; import graphql.schema.GraphQLSchema; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; import java.util.ArrayList; import java.util.HashMap; @@ -60,7 +62,7 @@ void recordTiming(String key, long time) { } } - class CustomInstrumentation extends SimpleInstrumentation { + class CustomInstrumentation extends SimplePerformantInstrumentation { @Override public InstrumentationState createState() { // @@ -71,7 +73,7 @@ public InstrumentationState createState() { } @Override - public InstrumentationContext beginExecution(InstrumentationExecutionParameters parameters) { + public @Nullable InstrumentationContext beginExecution(InstrumentationExecutionParameters parameters, InstrumentationState state) { long startNanos = System.nanoTime(); return new SimpleInstrumentationContext() { @Override @@ -83,7 +85,7 @@ public void onCompleted(ExecutionResult result, Throwable t) { } @Override - public DataFetcher instrumentDataFetcher(DataFetcher dataFetcher, InstrumentationFieldFetchParameters parameters) { + public @NotNull DataFetcher instrumentDataFetcher(DataFetcher dataFetcher, InstrumentationFieldFetchParameters parameters) { // // this allows you to intercept the data fetcher used to fetch a field and provide another one, perhaps // that enforces certain behaviours or has certain side effects on the data @@ -92,7 +94,7 @@ public DataFetcher instrumentDataFetcher(DataFetcher dataFetcher, Instrume } @Override - public CompletableFuture instrumentExecutionResult(ExecutionResult executionResult, InstrumentationExecutionParameters parameters) { + public @NotNull CompletableFuture instrumentExecutionResult(ExecutionResult executionResult, InstrumentationExecutionParameters parameters) { // // this allows you to instrument the execution result some how. For example the Tracing support uses this to put // the `extensions` map of data in place diff --git a/src/test/groovy/readme/ReadmeExamples.java b/src/test/groovy/readme/ReadmeExamples.java index 78072b3d44..6eb795279e 100644 --- a/src/test/groovy/readme/ReadmeExamples.java +++ b/src/test/groovy/readme/ReadmeExamples.java @@ -1,5 +1,6 @@ package readme; +import graphql.ErrorClassification; import graphql.GraphQLError; import graphql.GraphqlErrorBuilder; import graphql.InvalidSyntaxError; @@ -304,7 +305,7 @@ public Review get(DataFetchingEnvironment environment) { // be maps. You can convert them to POJOs inside the data fetcher if that // suits your code better // - // See http://facebook.github.io/graphql/October2016/#sec-Input-Objects + // See https://spec.graphql.org/October2021/#sec-Input-Objects // Map episodeInputMap = environment.getArgument("episode"); Map reviewInputMap = environment.getArgument("review"); @@ -470,7 +471,10 @@ public SpecialError build() { } private void errorBuilderExample() { - GraphQLError err = GraphqlErrorBuilder.newError().message("direct").build(); + GraphQLError err = GraphQLError.newError() + .message("direct") + .errorType(ErrorClassification.errorClassification("customClassification")) + .build(); SpecialError specialErr = new SpecialErrorBuilder().message("special").build(); } diff --git a/src/test/java/benchmark/AddError.java b/src/test/java/benchmark/AddError.java index b1c53dbc9d..950b756ab3 100644 --- a/src/test/java/benchmark/AddError.java +++ b/src/test/java/benchmark/AddError.java @@ -18,7 +18,7 @@ @State(Scope.Benchmark) public class AddError { - private ExecutionContext context = new ExecutionContextBuilder() + private final ExecutionContext context = new ExecutionContextBuilder() .executionId(ExecutionId.generate()) .build(); diff --git a/src/test/java/benchmark/BenchMark.java b/src/test/java/benchmark/BenchMark.java index 95ee823cad..626fafc87f 100644 --- a/src/test/java/benchmark/BenchMark.java +++ b/src/test/java/benchmark/BenchMark.java @@ -17,8 +17,6 @@ import org.openjdk.jmh.annotations.OutputTimeUnit; import org.openjdk.jmh.annotations.Warmup; -import java.io.InputStream; -import java.io.InputStreamReader; import java.util.ArrayList; import java.util.List; import java.util.concurrent.TimeUnit; @@ -44,7 +42,7 @@ public class BenchMark { @Benchmark @BenchmarkMode(Mode.Throughput) @OutputTimeUnit(TimeUnit.SECONDS) - public void benchMarkSimpleQueriesThroughput() { + public void benchMarkSimpleQueriesThroughput() { executeQuery(); } @@ -61,8 +59,7 @@ public static void executeQuery() { } private static GraphQL buildGraphQL() { - InputStream sdl = BenchMark.class.getClassLoader().getResourceAsStream("starWarsSchema.graphqls"); - TypeDefinitionRegistry definitionRegistry = new SchemaParser().parse(new InputStreamReader(sdl)); + TypeDefinitionRegistry definitionRegistry = new SchemaParser().parse(BenchmarkUtils.loadResource("starWarsSchema.graphqls")); DataFetcher heroDataFetcher = environment -> CharacterDTO.mkCharacter(environment, "r2d2", NUMBER_OF_FRIENDS); diff --git a/src/test/java/benchmark/BenchmarkUtils.java b/src/test/java/benchmark/BenchmarkUtils.java index ec7b44a1fc..fd7897e125 100644 --- a/src/test/java/benchmark/BenchmarkUtils.java +++ b/src/test/java/benchmark/BenchmarkUtils.java @@ -1,6 +1,7 @@ package benchmark; import com.google.common.io.Files; +import graphql.Assert; import java.io.File; import java.net.URL; @@ -9,11 +10,14 @@ public class BenchmarkUtils { + @SuppressWarnings("UnstableApiUsage") static String loadResource(String name) { return asRTE(() -> { URL resource = BenchmarkUtils.class.getClassLoader().getResource(name); + if (resource == null) { + throw new IllegalArgumentException("missing resource: " + name); + } return String.join("\n", Files.readLines(new File(resource.toURI()), Charset.defaultCharset())); - }); } diff --git a/src/test/java/benchmark/DFSelectionSetBenchmark.java b/src/test/java/benchmark/DFSelectionSetBenchmark.java index 1e54b32647..2687a444a0 100644 --- a/src/test/java/benchmark/DFSelectionSetBenchmark.java +++ b/src/test/java/benchmark/DFSelectionSetBenchmark.java @@ -1,7 +1,5 @@ package benchmark; -import com.google.common.base.Charsets; -import com.google.common.io.Resources; import graphql.execution.CoercedVariables; import graphql.language.Document; import graphql.normalized.ExecutableNormalizedField; @@ -27,13 +25,9 @@ import org.openjdk.jmh.annotations.Warmup; import org.openjdk.jmh.infra.Blackhole; -import java.io.IOException; -import java.net.URL; import java.util.List; import java.util.concurrent.TimeUnit; -import static com.google.common.io.Resources.getResource; - @State(Scope.Benchmark) @BenchmarkMode(Mode.Throughput) @Warmup(iterations = 2) @@ -51,10 +45,10 @@ public static class MyState { @Setup public void setup() { try { - String schemaString = readFromClasspath("large-schema-2.graphqls"); + String schemaString = BenchmarkUtils.loadResource("large-schema-2.graphqls"); schema = SchemaGenerator.createdMockedSchema(schemaString); - String query = readFromClasspath("large-schema-2-query.graphql"); + String query = BenchmarkUtils.loadResource("large-schema-2-query.graphql"); document = Parser.parse(query); ExecutableNormalizedOperation executableNormalizedOperation = ExecutableNormalizedOperationFactory.createExecutableNormalizedOperation(schema, document, null, CoercedVariables.emptyVariables()); @@ -64,15 +58,10 @@ public void setup() { outputFieldType = schema.getObjectType("Object42"); } catch (Exception e) { - System.out.println(e); throw new RuntimeException(e); } } - private String readFromClasspath(String file) throws IOException { - URL url = getResource(file); - return Resources.toString(url, Charsets.UTF_8); - } } @Benchmark diff --git a/src/test/java/benchmark/GetterAccessBenchmark.java b/src/test/java/benchmark/GetterAccessBenchmark.java new file mode 100644 index 0000000000..7dd8c3d719 --- /dev/null +++ b/src/test/java/benchmark/GetterAccessBenchmark.java @@ -0,0 +1,71 @@ +package benchmark; + +import graphql.schema.fetching.LambdaFetchingSupport; +import org.openjdk.jmh.annotations.Benchmark; +import org.openjdk.jmh.annotations.Measurement; +import org.openjdk.jmh.annotations.Warmup; +import org.openjdk.jmh.infra.Blackhole; + +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Method; +import java.util.function.Function; + +@Warmup(iterations = 2, time = 2, batchSize = 3) +@Measurement(iterations = 3, time = 2, batchSize = 4) +public class GetterAccessBenchmark { + + public static class Pojo { + final String name; + final int age; + + public Pojo(String name, int age) { + this.name = name; + this.age = age; + } + + public String getName() { + return name; + } + + public int getAge() { + return age; + } + } + + static Pojo pojo = new Pojo("Brad", 42); + + static Function getter = LambdaFetchingSupport.createGetter(Pojo.class, "name").get(); + + static Method getterMethod; + + static { + try { + getterMethod = Pojo.class.getMethod("getName"); + } catch (NoSuchMethodException e) { + throw new RuntimeException(e); + } + } + + + @Benchmark + public void measureDirectAccess(Blackhole bh) { + Object name = pojo.getName(); + bh.consume(name); + } + + @Benchmark + public void measureLambdaAccess(Blackhole bh) { + Object value = getter.apply(pojo); + bh.consume(value); + } + + @Benchmark + public void measureReflectionAccess(Blackhole bh) { + try { + Object name = getterMethod.invoke(pojo); + bh.consume(name); + } catch (IllegalAccessException | InvocationTargetException e) { + throw new RuntimeException(e); + } + } +} diff --git a/src/test/java/benchmark/IntrospectionBenchmark.java b/src/test/java/benchmark/IntrospectionBenchmark.java index 41dde6bc82..6745d07d62 100644 --- a/src/test/java/benchmark/IntrospectionBenchmark.java +++ b/src/test/java/benchmark/IntrospectionBenchmark.java @@ -1,11 +1,10 @@ package benchmark; -import com.google.common.base.Charsets; -import com.google.common.io.Resources; import graphql.ExecutionResult; import graphql.GraphQL; import graphql.execution.DataFetcherResult; -import graphql.execution.instrumentation.SimpleInstrumentation; +import graphql.execution.instrumentation.InstrumentationState; +import graphql.execution.instrumentation.SimplePerformantInstrumentation; import graphql.execution.instrumentation.parameters.InstrumentationFieldFetchParameters; import graphql.introspection.IntrospectionQuery; import graphql.schema.DataFetcher; @@ -13,6 +12,7 @@ import graphql.schema.GraphQLNamedType; import graphql.schema.GraphQLSchema; import graphql.schema.idl.SchemaGenerator; +import org.jetbrains.annotations.NotNull; import org.openjdk.jmh.annotations.Benchmark; import org.openjdk.jmh.annotations.BenchmarkMode; import org.openjdk.jmh.annotations.Measurement; @@ -21,27 +21,23 @@ import org.openjdk.jmh.annotations.State; import org.openjdk.jmh.annotations.Warmup; -import java.io.IOException; -import java.net.URL; import java.util.ArrayList; import java.util.LinkedHashMap; import java.util.List; import java.util.Map; -import static com.google.common.io.Resources.getResource; - @State(Scope.Benchmark) public class IntrospectionBenchmark { private final GraphQL graphQL; private final DFCountingInstrumentation countingInstrumentation = new DFCountingInstrumentation(); - static class DFCountingInstrumentation extends SimpleInstrumentation { + static class DFCountingInstrumentation extends SimplePerformantInstrumentation { Map counts = new LinkedHashMap<>(); Map times = new LinkedHashMap<>(); @Override - public DataFetcher instrumentDataFetcher(DataFetcher dataFetcher, InstrumentationFieldFetchParameters parameters) { + public @NotNull DataFetcher instrumentDataFetcher(DataFetcher dataFetcher, InstrumentationFieldFetchParameters parameters, InstrumentationState state) { return (DataFetcher) env -> { long then = System.nanoTime(); Object value = dataFetcher.get(env); @@ -77,23 +73,13 @@ private boolean isSchemaTypesFetch(DataFetchingEnvironment env, Object source) { } public IntrospectionBenchmark() { - String largeSchema = readFromClasspath("large-schema-4.graphqls"); + String largeSchema = BenchmarkUtils.loadResource("large-schema-4.graphqls"); GraphQLSchema graphQLSchema = SchemaGenerator.createdMockedSchema(largeSchema); graphQL = GraphQL.newGraphQL(graphQLSchema) //.instrumentation(countingInstrumentation) .build(); } - - private static String readFromClasspath(String file) { - URL url = getResource(file); - try { - return Resources.toString(url, Charsets.UTF_8); - } catch (IOException e) { - throw new RuntimeException(e); - } - } - public static void main(String[] args) { IntrospectionBenchmark introspectionBenchmark = new IntrospectionBenchmark(); // while (true) { diff --git a/src/test/java/benchmark/NQBenchmark1.java b/src/test/java/benchmark/NQBenchmark1.java index f96dd27447..d47d7b7a4c 100644 --- a/src/test/java/benchmark/NQBenchmark1.java +++ b/src/test/java/benchmark/NQBenchmark1.java @@ -1,7 +1,5 @@ package benchmark; -import com.google.common.base.Charsets; -import com.google.common.io.Resources; import graphql.execution.CoercedVariables; import graphql.language.Document; import graphql.normalized.ExecutableNormalizedOperation; @@ -22,13 +20,8 @@ import org.openjdk.jmh.annotations.Warmup; import org.openjdk.jmh.infra.Blackhole; -import java.io.IOException; -import java.net.URL; -import java.util.Collections; import java.util.concurrent.TimeUnit; -import static com.google.common.io.Resources.getResource; - @State(Scope.Benchmark) @BenchmarkMode(Mode.Throughput) @Warmup(iterations = 2) @@ -44,21 +37,15 @@ public static class MyState { @Setup public void setup() { try { - String schemaString = readFromClasspath("large-schema-1.graphqls"); + String schemaString = BenchmarkUtils.loadResource("large-schema-1.graphqls"); schema = SchemaGenerator.createdMockedSchema(schemaString); - String query = readFromClasspath("large-schema-1-query.graphql"); + String query = BenchmarkUtils.loadResource("large-schema-1-query.graphql"); document = Parser.parse(query); } catch (Exception e) { - System.out.println(e); throw new RuntimeException(e); } } - - private String readFromClasspath(String file) throws IOException { - URL url = getResource(file); - return Resources.toString(url, Charsets.UTF_8); - } } @Benchmark diff --git a/src/test/java/benchmark/NQBenchmark2.java b/src/test/java/benchmark/NQBenchmark2.java index 931e1160f9..68402b4ec7 100644 --- a/src/test/java/benchmark/NQBenchmark2.java +++ b/src/test/java/benchmark/NQBenchmark2.java @@ -1,8 +1,6 @@ package benchmark; -import com.google.common.base.Charsets; import com.google.common.collect.ImmutableListMultimap; -import com.google.common.io.Resources; import graphql.execution.CoercedVariables; import graphql.language.Document; import graphql.language.Field; @@ -28,16 +26,10 @@ import org.openjdk.jmh.annotations.Threads; import org.openjdk.jmh.annotations.Warmup; -import java.io.IOException; -import java.net.URL; import java.util.ArrayList; -import java.util.Collections; import java.util.List; -import java.util.concurrent.ExecutionException; import java.util.concurrent.TimeUnit; -import static com.google.common.io.Resources.getResource; - @State(Scope.Benchmark) @BenchmarkMode(Mode.Throughput) @Warmup(iterations = 2) @@ -53,21 +45,16 @@ public static class MyState { @Setup public void setup() { try { - String schemaString = readFromClasspath("large-schema-2.graphqls"); + String schemaString = BenchmarkUtils.loadResource("large-schema-2.graphqls"); schema = SchemaGenerator.createdMockedSchema(schemaString); - String query = readFromClasspath("large-schema-2-query.graphql"); + String query = BenchmarkUtils.loadResource("large-schema-2-query.graphql"); document = Parser.parse(query); } catch (Exception e) { - System.out.println(e); throw new RuntimeException(e); } } - private String readFromClasspath(String file) throws IOException { - URL url = getResource(file); - return Resources.toString(url, Charsets.UTF_8); - } } @Benchmark @@ -77,7 +64,7 @@ private String readFromClasspath(String file) throws IOException { @Fork(3) @BenchmarkMode(Mode.AverageTime) @OutputTimeUnit(TimeUnit.MILLISECONDS) - public ExecutableNormalizedOperation benchMarkAvgTime(MyState myState) throws ExecutionException, InterruptedException { + public ExecutableNormalizedOperation benchMarkAvgTime(MyState myState) { ExecutableNormalizedOperation executableNormalizedOperation = ExecutableNormalizedOperationFactory.createExecutableNormalizedOperation(myState.schema, myState.document, null, CoercedVariables.emptyVariables()); // System.out.println("fields size:" + normalizedQuery.getFieldToNormalizedField().size()); return executableNormalizedOperation; diff --git a/src/test/java/benchmark/NQExtraLargeBenchmark.java b/src/test/java/benchmark/NQExtraLargeBenchmark.java index 39714382a0..fb92dbda9d 100644 --- a/src/test/java/benchmark/NQExtraLargeBenchmark.java +++ b/src/test/java/benchmark/NQExtraLargeBenchmark.java @@ -1,7 +1,5 @@ package benchmark; -import com.google.common.base.Charsets; -import com.google.common.io.Resources; import graphql.execution.CoercedVariables; import graphql.language.Document; import graphql.normalized.ExecutableNormalizedOperation; @@ -22,12 +20,8 @@ import org.openjdk.jmh.annotations.Warmup; import org.openjdk.jmh.infra.Blackhole; -import java.io.IOException; -import java.net.URL; import java.util.concurrent.TimeUnit; -import static com.google.common.io.Resources.getResource; - @State(Scope.Benchmark) @BenchmarkMode(Mode.Throughput) @Warmup(iterations = 2) @@ -43,21 +37,15 @@ public static class MyState { @Setup public void setup() { try { - String schemaString = readFromClasspath("extra-large-schema-1.graphqls"); + String schemaString = BenchmarkUtils.loadResource("extra-large-schema-1.graphqls"); schema = SchemaGenerator.createdMockedSchema(schemaString); - String query = readFromClasspath("extra-large-schema-1-query.graphql"); + String query = BenchmarkUtils.loadResource("extra-large-schema-1-query.graphql"); document = Parser.parse(query); } catch (Exception e) { - System.out.println(e); throw new RuntimeException(e); } } - - private String readFromClasspath(String file) throws IOException { - URL url = getResource(file); - return Resources.toString(url, Charsets.UTF_8); - } } @Benchmark diff --git a/src/test/java/benchmark/OverlappingFieldValidationBenchmark.java b/src/test/java/benchmark/OverlappingFieldValidationBenchmark.java index 87c4579102..d8cf18bb5f 100644 --- a/src/test/java/benchmark/OverlappingFieldValidationBenchmark.java +++ b/src/test/java/benchmark/OverlappingFieldValidationBenchmark.java @@ -1,7 +1,5 @@ package benchmark; -import com.google.common.base.Charsets; -import com.google.common.io.Resources; import graphql.ExecutionResult; import graphql.GraphQL; import graphql.i18n.I18n; @@ -28,14 +26,11 @@ import org.openjdk.jmh.annotations.Warmup; import org.openjdk.jmh.infra.Blackhole; -import java.io.IOException; -import java.net.URL; import java.util.Collections; import java.util.List; import java.util.Locale; import java.util.concurrent.TimeUnit; -import static com.google.common.io.Resources.getResource; import static graphql.Assert.assertTrue; @State(Scope.Benchmark) @@ -56,8 +51,8 @@ public static class MyState { @Setup public void setup() { try { - String schemaString = readFromClasspath("large-schema-4.graphqls"); - String query = readFromClasspath("large-schema-4-query.graphql"); + String schemaString = BenchmarkUtils.loadResource("large-schema-4.graphqls"); + String query = BenchmarkUtils.loadResource("large-schema-4-query.graphql"); schema = SchemaGenerator.createdMockedSchema(schemaString); document = Parser.parse(query); @@ -66,15 +61,9 @@ public void setup() { ExecutionResult executionResult = graphQL.execute(query); assertTrue(executionResult.getErrors().size() == 0); } catch (Exception e) { - System.out.println(e); throw new RuntimeException(e); } } - - private String readFromClasspath(String file) throws IOException { - URL url = getResource(file); - return Resources.toString(url, Charsets.UTF_8); - } } @Benchmark diff --git a/src/test/java/benchmark/SchemaBenchMark.java b/src/test/java/benchmark/SchemaBenchMark.java index 54636eb53d..23d35427e8 100644 --- a/src/test/java/benchmark/SchemaBenchMark.java +++ b/src/test/java/benchmark/SchemaBenchMark.java @@ -33,7 +33,7 @@ @Measurement(iterations = 3, time = 10, batchSize = 4) public class SchemaBenchMark { - static String largeSDL = createResourceSDL("large-schema-3.graphqls"); + static String largeSDL = BenchmarkUtils.loadResource("large-schema-3.graphqls"); @Benchmark @BenchmarkMode(Mode.Throughput) @@ -54,17 +54,6 @@ private static GraphQLSchema createSchema(String sdl) { return new SchemaGenerator().makeExecutableSchema(registry, RuntimeWiring.MOCKED_WIRING); } - private static String createResourceSDL(String name) { - try { - URL resource = SchemaBenchMark.class.getClassLoader().getResource(name); - File file = new File(resource.toURI()); - return String.join("\n", Files.readLines(file, Charset.defaultCharset())); - } catch (Exception e) { - throw new RuntimeException(e); - } - - } - @SuppressWarnings("InfiniteLoopStatement") /// make this a main method if you want to run it in JProfiler etc.. public static void mainXXX(String[] args) { diff --git a/src/test/java/benchmark/SchemaTransformerBenchmark.java b/src/test/java/benchmark/SchemaTransformerBenchmark.java index c715d63d75..96057aa534 100644 --- a/src/test/java/benchmark/SchemaTransformerBenchmark.java +++ b/src/test/java/benchmark/SchemaTransformerBenchmark.java @@ -100,19 +100,14 @@ public TraversalControl visitGraphQLObjectType(GraphQLObjectType node, Traverser @Setup public void setup() { try { - String schemaString = readFromClasspath("large-schema-3.graphqls"); + String schemaString = BenchmarkUtils.loadResource("large-schema-3.graphqls"); schema = SchemaGenerator.createdMockedSchema(schemaString); txSchema = SchemaTransformer.transformSchema(schema, directiveAdder); } catch (Exception e) { - System.out.println(e); throw new RuntimeException(e); } } - private String readFromClasspath(String file) throws IOException { - URL url = getResource(file); - return Resources.toString(url, Charsets.UTF_8); - } } @Benchmark diff --git a/src/test/java/benchmark/TwitterBenchmark.java b/src/test/java/benchmark/TwitterBenchmark.java index 21444a9f58..4164f8a40b 100644 --- a/src/test/java/benchmark/TwitterBenchmark.java +++ b/src/test/java/benchmark/TwitterBenchmark.java @@ -1,55 +1,34 @@ package benchmark; -import java.io.InputStream; -import java.io.InputStreamReader; -import java.util.ArrayList; -import java.util.HashMap; -import java.util.List; -import java.util.Map; -import java.util.Optional; -import java.util.concurrent.TimeUnit; -import java.util.function.Function; - -import com.github.javafaker.Code; - -import org.openjdk.jmh.annotations.Benchmark; -import org.openjdk.jmh.annotations.BenchmarkMode; -import org.openjdk.jmh.annotations.Fork; -import org.openjdk.jmh.annotations.Measurement; -import org.openjdk.jmh.annotations.Mode; -import org.openjdk.jmh.annotations.OutputTimeUnit; -import org.openjdk.jmh.annotations.Threads; -import org.openjdk.jmh.annotations.Warmup; -import org.openjdk.jmh.infra.Blackhole; - import graphql.ExecutionInput; import graphql.ExecutionResult; import graphql.GraphQL; -import graphql.execution.ExecutionStepInfo; -import graphql.execution.instrumentation.tracing.TracingInstrumentation; -import graphql.execution.preparsed.PreparsedDocumentEntry; -import graphql.execution.preparsed.PreparsedDocumentProvider; import graphql.execution.preparsed.persisted.InMemoryPersistedQueryCache; import graphql.execution.preparsed.persisted.PersistedQueryCache; import graphql.execution.preparsed.persisted.PersistedQuerySupport; +import graphql.parser.ParserOptions; import graphql.schema.DataFetcher; -import graphql.schema.DataFetcherFactory; -import graphql.schema.DataFetcherFactoryEnvironment; -import graphql.schema.DataFetchingEnvironment; -import graphql.schema.FieldCoordinates; import graphql.schema.GraphQLCodeRegistry; import graphql.schema.GraphQLFieldDefinition; import graphql.schema.GraphQLObjectType; import graphql.schema.GraphQLSchema; import graphql.schema.GraphQLTypeReference; -import graphql.schema.idl.RuntimeWiring; -import graphql.schema.idl.SchemaGenerator; -import graphql.schema.idl.SchemaGeneratorHelper; -import graphql.schema.idl.SchemaParser; -import graphql.schema.idl.TypeDefinitionRegistry; +import org.openjdk.jmh.annotations.Benchmark; +import org.openjdk.jmh.annotations.BenchmarkMode; +import org.openjdk.jmh.annotations.Fork; +import org.openjdk.jmh.annotations.Measurement; +import org.openjdk.jmh.annotations.Mode; +import org.openjdk.jmh.annotations.OutputTimeUnit; +import org.openjdk.jmh.annotations.Threads; +import org.openjdk.jmh.annotations.Warmup; +import org.openjdk.jmh.infra.Blackhole; + +import java.util.ArrayList; +import java.util.List; +import java.util.Optional; +import java.util.concurrent.TimeUnit; import static graphql.Scalars.GraphQLString; -import static graphql.schema.idl.TypeRuntimeWiring.newTypeWiring; @Warmup(iterations = 8, time = 10) @Measurement(iterations = 25, time = 10) @@ -94,6 +73,8 @@ public static String mkQuery() { } private static GraphQL buildGraphQL() { + ParserOptions.setDefaultOperationParserOptions(ParserOptions.newParserOptions().maxTokens(100_000).build()); + List leafFields = new ArrayList<>(BREADTH); for (int i = 1; i <= BREADTH; i++) { leafFields.add( @@ -116,12 +97,7 @@ private static GraphQL buildGraphQL() { DataFetcher simpleFetcher = env -> env.getField().getName(); GraphQLCodeRegistry codeReg = GraphQLCodeRegistry.newCodeRegistry() .defaultDataFetcher( - new DataFetcherFactory() { - @Override - public DataFetcher get(DataFetcherFactoryEnvironment environment) { - return simpleFetcher; - } - } + environment -> simpleFetcher ) .build(); @@ -156,6 +132,6 @@ protected Optional getPersistedQueryId(ExecutionInput executionInput) { public static void main(String[] args) { ExecutionResult result = execute(); - int i = 0; + System.out.println(result); } } diff --git a/src/test/java/benchmark/ValidatorBenchmark.java b/src/test/java/benchmark/ValidatorBenchmark.java index e07f544239..9db2384f29 100644 --- a/src/test/java/benchmark/ValidatorBenchmark.java +++ b/src/test/java/benchmark/ValidatorBenchmark.java @@ -1,13 +1,8 @@ package benchmark; -import java.io.IOException; -import java.net.URL; import java.util.Locale; import java.util.concurrent.TimeUnit; -import com.google.common.base.Charsets; -import com.google.common.io.Resources; - import org.openjdk.jmh.annotations.Benchmark; import org.openjdk.jmh.annotations.BenchmarkMode; import org.openjdk.jmh.annotations.Fork; @@ -28,8 +23,6 @@ import graphql.schema.idl.SchemaGenerator; import graphql.validation.Validator; -import static com.google.common.io.Resources.getResource; - import static graphql.Assert.assertTrue; @@ -67,8 +60,8 @@ public void setup() { private Scenario load(String schemaPath, String queryPath) { try { - String schemaString = readFromClasspath(schemaPath); - String query = readFromClasspath(queryPath); + String schemaString = BenchmarkUtils.loadResource(schemaPath); + String query = BenchmarkUtils.loadResource(queryPath); GraphQLSchema schema = SchemaGenerator.createdMockedSchema(schemaString); Document document = Parser.parse(query); @@ -78,15 +71,10 @@ private Scenario load(String schemaPath, String queryPath) { assertTrue(executionResult.getErrors().size() == 0); return new Scenario(schema, document); } catch (Exception e) { - System.out.println(e); throw new RuntimeException(e); } } - private String readFromClasspath(String file) throws IOException { - URL url = getResource(file); - return Resources.toString(url, Charsets.UTF_8); - } } private void run(Scenario scenario) { diff --git a/src/test/resources/diff/schema_ABaseLine.graphqls b/src/test/resources/diff/schema_ABaseLine.graphqls index 7444225c96..59806923f1 100644 --- a/src/test/resources/diff/schema_ABaseLine.graphqls +++ b/src/test/resources/diff/schema_ABaseLine.graphqls @@ -12,6 +12,7 @@ type Query { deities : [Deity] allCharacters : [Character!] @deprecated(reason: "no longer supported") + allCharactersByTemperament : [[Character]] customScalar : CustomScalar } @@ -22,7 +23,7 @@ type Mutation { } input Questor { - beingID : ID + beingID : ID! queryTarget : String nestedInput : NestedInput } diff --git a/src/test/resources/diff/schema_changed_field_arguments.graphqls b/src/test/resources/diff/schema_changed_field_arguments.graphqls index 43c919dd33..e8c0c260fe 100644 --- a/src/test/resources/diff/schema_changed_field_arguments.graphqls +++ b/src/test/resources/diff/schema_changed_field_arguments.graphqls @@ -12,6 +12,7 @@ type Query { deities : [Deity] allCharacters : [Character!] @deprecated(reason: "no longer supported") + allCharactersByTemperament : [[Character]] customScalar : CustomScalar } @@ -23,7 +24,7 @@ type Mutation { } input Questor { - beingID : ID + beingID : ID! queryTarget : String nestedInput : NestedInput } diff --git a/src/test/resources/diff/schema_changed_input_object_fields.graphqls b/src/test/resources/diff/schema_changed_input_object_fields.graphqls index 7d6af1b074..d7d026ffa8 100644 --- a/src/test/resources/diff/schema_changed_input_object_fields.graphqls +++ b/src/test/resources/diff/schema_changed_input_object_fields.graphqls @@ -4,7 +4,8 @@ schema { } type Query { - being(id : ID, type : String = "wizard") : Being + #being(id : ID, type : String = "wizard") : Being + being(id : ID, type : String, newArg : String!) : Being beings(type : String) : [Being] wizards : [Istari] @@ -12,6 +13,7 @@ type Query { deities : [Deity] allCharacters : [Character!] @deprecated(reason: "no longer supported") + allCharactersByTemperament : [[Character]] customScalar : CustomScalar } @@ -22,9 +24,14 @@ type Mutation { } input Questor { + #beingID : ID! beingID : ID + + #queryTarget : String queryTarget : Int - nestedInput : NestedInput + + #nestedInput : NestedInput + nestedInput : NestedInput! newMandatoryField : String! } diff --git a/src/test/resources/diff/schema_changed_nested_input_object_fields.graphqls b/src/test/resources/diff/schema_changed_nested_input_object_fields.graphqls index 1ac57e90c5..d3009f7db7 100644 --- a/src/test/resources/diff/schema_changed_nested_input_object_fields.graphqls +++ b/src/test/resources/diff/schema_changed_nested_input_object_fields.graphqls @@ -12,6 +12,7 @@ type Query { deities : [Deity] allCharacters : [Character!] @deprecated(reason: "no longer supported") + allCharactersByTemperament : [[Character]] customScalar : CustomScalar } @@ -22,7 +23,7 @@ type Mutation { } input Questor { - beingID : ID + beingID : ID! queryTarget : String nestedInput : NestedInput } diff --git a/src/test/resources/diff/schema_changed_object_fields.graphqls b/src/test/resources/diff/schema_changed_object_fields.graphqls index d7a86306de..4c5f273dca 100644 --- a/src/test/resources/diff/schema_changed_object_fields.graphqls +++ b/src/test/resources/diff/schema_changed_object_fields.graphqls @@ -4,8 +4,7 @@ schema { } type Query { - #being(id : ID, type : String = "wizard") : Being - being(id : ID, type : String, newArg : String!) : Being + being(id : ID, type : String = "wizard") : Being #beings(type : String) : [Being] beings(type : String) : Being @@ -18,6 +17,9 @@ type Query { #allCharacters : [Character!] @deprecated(reason: "no longer supported") allCharacters : [Character!] + #allCharactersByTemperament : [[Character]] + allCharactersByTemperament : [[Character!]]! + #customScalar : CustomScalar customScalar : [CustomScalar]! } @@ -28,7 +30,7 @@ type Mutation { } input Questor { - beingID : ID + beingID : ID! queryTarget : String nestedInput : NestedInput } @@ -41,7 +43,8 @@ scalar CustomScalar interface Being { - id : ID + #id : ID + id : ID! name : String nameInQuenyan : String invitedBy(id : ID) : Being @@ -49,7 +52,8 @@ interface Being { type Ainur implements Being { - id : ID + #id : ID + id : ID! name : String nameInQuenyan : String invitedBy(id : ID) : Being @@ -58,17 +62,21 @@ type Ainur implements Being { type Istari implements Being { - id : ID + #id : ID + id : ID! name : String nameInQuenyan : String invitedBy(id : ID) : Being colour : String - temperament : Temperament! + + #temperament : Temperament! + temperament : Temperament } type Deity implements Being { - id : ID + #id : ID + id : ID! name : String nameInQuenyan : String invitedBy(id : ID) : Being diff --git a/src/test/resources/diff/schema_changed_type_kind.graphqls b/src/test/resources/diff/schema_changed_type_kind.graphqls index 5107c3ed39..14e85658f9 100644 --- a/src/test/resources/diff/schema_changed_type_kind.graphqls +++ b/src/test/resources/diff/schema_changed_type_kind.graphqls @@ -12,6 +12,7 @@ type Query { deities : [Deity] allCharacters : [Character!] @deprecated(reason: "no longer supported") + allCharactersByTemperament : [[Character]] customScalar : CustomScalar } @@ -22,7 +23,7 @@ type Mutation { } input Questor { - beingID : ID + beingID : ID! queryTarget : String nestedInput : NestedInput } diff --git a/src/test/resources/diff/schema_dangerous_changes.graphqls b/src/test/resources/diff/schema_dangerous_changes.graphqls index cb910f80c0..1fa8b8808e 100644 --- a/src/test/resources/diff/schema_dangerous_changes.graphqls +++ b/src/test/resources/diff/schema_dangerous_changes.graphqls @@ -12,6 +12,7 @@ type Query { deities : [Deity] allCharacters : [Character!] @deprecated(reason: "no longer supported") + allCharactersByTemperament : [[Character]] customScalar : CustomScalar } @@ -22,7 +23,7 @@ type Mutation { } input Questor { - beingID : ID + beingID : ID! queryTarget : String nestedInput : NestedInput } diff --git a/src/test/resources/diff/schema_deprecated_fields_new.graphqls b/src/test/resources/diff/schema_deprecated_fields_new.graphqls index b3d92e6d15..53dc321ede 100644 --- a/src/test/resources/diff/schema_deprecated_fields_new.graphqls +++ b/src/test/resources/diff/schema_deprecated_fields_new.graphqls @@ -12,6 +12,7 @@ type Query { deities : [Deity] @deprecated allCharacters : [Character!] @deprecated(reason: "no longer supported") + allCharactersByTemperament : [[Character]] @deprecated(reason: "remove all Character references") customScalar : CustomScalar @deprecated(reason: "because") } diff --git a/src/test/resources/diff/schema_interface_fields_missing.graphqls b/src/test/resources/diff/schema_interface_fields_missing.graphqls index b8e0f043fb..15f45aacdf 100644 --- a/src/test/resources/diff/schema_interface_fields_missing.graphqls +++ b/src/test/resources/diff/schema_interface_fields_missing.graphqls @@ -12,6 +12,7 @@ type Query { deities : [Deity] allCharacters : [Character!] @deprecated(reason: "no longer supported") + allCharactersByTemperament : [[Character]] customScalar : CustomScalar } @@ -22,7 +23,7 @@ type Mutation { } input Questor { - beingID : ID + beingID : ID! queryTarget : String nestedInput : NestedInput } diff --git a/src/test/resources/diff/schema_missing_enum_value.graphqls b/src/test/resources/diff/schema_missing_enum_value.graphqls index f69185f429..7dcf130ab5 100644 --- a/src/test/resources/diff/schema_missing_enum_value.graphqls +++ b/src/test/resources/diff/schema_missing_enum_value.graphqls @@ -12,6 +12,7 @@ type Query { deities : [Deity] allCharacters : [Character!] @deprecated(reason: "no longer supported") + allCharactersByTemperament : [[Character]] customScalar : CustomScalar } @@ -22,7 +23,7 @@ type Mutation { } input Questor { - beingID : ID + beingID : ID! queryTarget : String nestedInput : NestedInput } diff --git a/src/test/resources/diff/schema_missing_field_arguments.graphqls b/src/test/resources/diff/schema_missing_field_arguments.graphqls index 40c5ad21e9..1657a86eed 100644 --- a/src/test/resources/diff/schema_missing_field_arguments.graphqls +++ b/src/test/resources/diff/schema_missing_field_arguments.graphqls @@ -12,6 +12,7 @@ type Query { deities : [Deity] allCharacters : [Character!] @deprecated(reason: "no longer supported") + allCharactersByTemperament : [[Character]] customScalar : CustomScalar } @@ -24,7 +25,7 @@ type Mutation { } input Questor { - beingID : ID + beingID : ID! queryTarget : String nestedInput : NestedInput } diff --git a/src/test/resources/diff/schema_missing_input_object_fields.graphqls b/src/test/resources/diff/schema_missing_input_object_fields.graphqls index 543e4f4048..c06aa426d4 100644 --- a/src/test/resources/diff/schema_missing_input_object_fields.graphqls +++ b/src/test/resources/diff/schema_missing_input_object_fields.graphqls @@ -12,6 +12,7 @@ type Query { deities : [Deity] allCharacters : [Character!] @deprecated(reason: "no longer supported") + allCharactersByTemperament : [[Character]] customScalar : CustomScalar } @@ -22,7 +23,7 @@ type Mutation { } input Questor { - beingID : ID + beingID : ID! #queryTarget : String nestedInput : NestedInput } diff --git a/src/test/resources/diff/schema_missing_object_fields.graphqls b/src/test/resources/diff/schema_missing_object_fields.graphqls index d033ac0a1d..073de2507d 100644 --- a/src/test/resources/diff/schema_missing_object_fields.graphqls +++ b/src/test/resources/diff/schema_missing_object_fields.graphqls @@ -12,6 +12,7 @@ type Query { deities : [Deity] allCharacters : [Character!] @deprecated(reason: "no longer supported") + allCharactersByTemperament : [[Character]] customScalar : CustomScalar } @@ -22,7 +23,7 @@ type Mutation { } input Questor { - beingID : ID + beingID : ID! queryTarget : String nestedInput : NestedInput } diff --git a/src/test/resources/diff/schema_missing_operation.graphqls b/src/test/resources/diff/schema_missing_operation.graphqls index 9e83a2eb6a..7b4ac24077 100644 --- a/src/test/resources/diff/schema_missing_operation.graphqls +++ b/src/test/resources/diff/schema_missing_operation.graphqls @@ -11,12 +11,13 @@ type Query { deities : [Deity] allCharacters : [Character!] @deprecated(reason: "no longer supported") + allCharactersByTemperament : [[Character]] customScalar : CustomScalar } input Questor { - beingID : ID + beingID : ID! queryTarget : String nestedInput : NestedInput } diff --git a/src/test/resources/diff/schema_missing_union_members.graphqls b/src/test/resources/diff/schema_missing_union_members.graphqls index dd6ea96050..51c3d2ea6a 100644 --- a/src/test/resources/diff/schema_missing_union_members.graphqls +++ b/src/test/resources/diff/schema_missing_union_members.graphqls @@ -12,6 +12,7 @@ type Query { deities : [Deity] allCharacters : [Character!] @deprecated(reason: "no longer supported") + allCharactersByTemperament : [[Character]] customScalar : CustomScalar } @@ -22,7 +23,7 @@ type Mutation { } input Questor { - beingID : ID + beingID : ID! queryTarget : String nestedInput : NestedInput } diff --git a/src/test/resources/diff/schema_with_additional_field.graphqls b/src/test/resources/diff/schema_with_additional_field.graphqls index 2001ba2f75..1e9c4daa1c 100644 --- a/src/test/resources/diff/schema_with_additional_field.graphqls +++ b/src/test/resources/diff/schema_with_additional_field.graphqls @@ -12,6 +12,7 @@ type Query { deities : [Deity] allCharacters : [Character!] @deprecated(reason: "no longer supported") + allCharactersByTemperament : [[Character]] customScalar : CustomScalar } @@ -22,7 +23,7 @@ type Mutation { } input Questor { - beingID : ID + beingID : ID! queryTarget : String nestedInput : NestedInput } diff --git a/src/test/resources/extra-large-schema-1.graphqls b/src/test/resources/extra-large-schema-1.graphqls index 4e7ba47bf8..0d32ac9cca 100644 --- a/src/test/resources/extra-large-schema-1.graphqls +++ b/src/test/resources/extra-large-schema-1.graphqls @@ -824,12 +824,10 @@ enum JiraAttachmentsPermissions { input JiraOrderDirection { id: ID - #TODO } input JiraAttachmentsOrderField { id: ID - #TODO } """ Represents the avatar size urls. @@ -1655,7 +1653,6 @@ type JiraComponent implements Node { Component description. """ description: String - # TODO: lead, default assignee ... } """