From 1e9231669fe0e96cd0a488fbd020d9e750f7b9ad Mon Sep 17 00:00:00 2001 From: jimkyndemeyer Date: Sun, 1 Jul 2018 18:33:57 +0200 Subject: [PATCH] Added tools for introspection (#164) - Introspection query execute line marker on url in .graphqlconfig - Print schema JSON as SDL line marker --- resources/META-INF/plugin.xml | 6 + .../lang/jsgraphql/GraphQLSettings.java | 11 +- ...trospectEndpointUrlLineMarkerProvider.java | 178 +++++++++++ .../editor/GraphQLIntrospectionHelper.java | 96 ++++++ ...rospectionJsonToSDLLineMarkerProvider.java | 78 +++++ .../GraphQLIntrospectionResultToSchema.java | 295 ++++++++++++++++++ ...ectionProjectViewNestingRulesProvider.java | 22 ++ .../GraphQLConfigPackageSet.java | 2 +- .../GraphQLConfigVariableAwareEndpoint.java | 2 +- .../ui/GraphQLProjectSettingsForm.form | 4 +- .../ui/GraphQLProjectSettingsForm.java | 6 +- .../JSGraphQLLanguageUIProjectService.java | 44 ++- 12 files changed, 724 insertions(+), 20 deletions(-) create mode 100644 src/main/com/intellij/lang/jsgraphql/ide/editor/GraphQLIntrospectEndpointUrlLineMarkerProvider.java create mode 100644 src/main/com/intellij/lang/jsgraphql/ide/editor/GraphQLIntrospectionHelper.java create mode 100644 src/main/com/intellij/lang/jsgraphql/ide/editor/GraphQLIntrospectionJsonToSDLLineMarkerProvider.java create mode 100644 src/main/com/intellij/lang/jsgraphql/ide/editor/GraphQLIntrospectionResultToSchema.java create mode 100644 src/main/com/intellij/lang/jsgraphql/ide/project/GraphQLIntrospectionProjectViewNestingRulesProvider.java diff --git a/resources/META-INF/plugin.xml b/resources/META-INF/plugin.xml index 6d63d789..31a7dd6f 100644 --- a/resources/META-INF/plugin.xml +++ b/resources/META-INF/plugin.xml @@ -102,6 +102,12 @@ + + + + + + diff --git a/src/main/com/intellij/lang/jsgraphql/GraphQLSettings.java b/src/main/com/intellij/lang/jsgraphql/GraphQLSettings.java index 56e75cda..9b70cc2b 100644 --- a/src/main/com/intellij/lang/jsgraphql/GraphQLSettings.java +++ b/src/main/com/intellij/lang/jsgraphql/GraphQLSettings.java @@ -43,7 +43,15 @@ public GraphQLScopeResolution getScopeResolution() { } public void setScopeResolution(GraphQLScopeResolution scopeResolution) { - this.myState.scopeResolution = scopeResolution; + myState.scopeResolution = scopeResolution; + } + + public String getIntrospectionQuery() { + return myState.introspectionQuery; + } + + public void setIntrospectionQuery(String introspectionQuery) { + myState.introspectionQuery = introspectionQuery; } /** @@ -53,6 +61,7 @@ public void setScopeResolution(GraphQLScopeResolution scopeResolution) { */ static class GraphQLSettingsState { public GraphQLScopeResolution scopeResolution = GraphQLScopeResolution.ENTIRE_PROJECT; + public String introspectionQuery = ""; } } diff --git a/src/main/com/intellij/lang/jsgraphql/ide/editor/GraphQLIntrospectEndpointUrlLineMarkerProvider.java b/src/main/com/intellij/lang/jsgraphql/ide/editor/GraphQLIntrospectEndpointUrlLineMarkerProvider.java new file mode 100644 index 00000000..2c553f1f --- /dev/null +++ b/src/main/com/intellij/lang/jsgraphql/ide/editor/GraphQLIntrospectEndpointUrlLineMarkerProvider.java @@ -0,0 +1,178 @@ +/* + * Copyright (c) 2018-present, Jim Kynde Meyer + * All rights reserved. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ +package com.intellij.lang.jsgraphql.ide.editor; + +import com.google.gson.Gson; +import com.intellij.codeHighlighting.Pass; +import com.intellij.codeInsight.daemon.LineMarkerInfo; +import com.intellij.codeInsight.daemon.LineMarkerProvider; +import com.intellij.icons.AllIcons; +import com.intellij.json.psi.JsonObject; +import com.intellij.json.psi.JsonProperty; +import com.intellij.json.psi.JsonStringLiteral; +import com.intellij.json.psi.JsonValue; +import com.intellij.lang.jsgraphql.GraphQLSettings; +import com.intellij.lang.jsgraphql.ide.project.graphqlconfig.GraphQLConfigManager; +import com.intellij.lang.jsgraphql.ide.project.graphqlconfig.model.GraphQLConfigEndpoint; +import com.intellij.lang.jsgraphql.ide.project.graphqlconfig.model.GraphQLConfigVariableAwareEndpoint; +import com.intellij.lang.jsgraphql.v1.ide.project.JSGraphQLLanguageUIProjectService; +import com.intellij.notification.Notification; +import com.intellij.notification.NotificationType; +import com.intellij.notification.Notifications; +import com.intellij.openapi.application.ApplicationManager; +import com.intellij.openapi.editor.markup.GutterIconRenderer; +import com.intellij.openapi.progress.ProgressManager; +import com.intellij.openapi.vfs.VirtualFile; +import com.intellij.psi.PsiElement; +import com.intellij.psi.util.PsiTreeUtil; +import graphql.introspection.IntrospectionQuery; +import org.apache.commons.httpclient.HttpClient; +import org.apache.commons.httpclient.methods.PostMethod; +import org.apache.commons.httpclient.methods.StringRequestEntity; +import org.apache.commons.httpclient.params.HttpClientParams; +import org.apache.commons.lang.StringEscapeUtils; +import org.apache.commons.lang.StringUtils; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +import java.io.IOException; +import java.io.UnsupportedEncodingException; +import java.util.Collection; +import java.util.List; +import java.util.Map; +import java.util.Optional; +import java.util.stream.Stream; + +import static com.intellij.lang.jsgraphql.ide.editor.GraphQLIntrospectionHelper.printIntrospectionJsonAsGraphQL; +import static com.intellij.lang.jsgraphql.v1.ide.project.JSGraphQLLanguageUIProjectService.setHeadersFromOptions; + +/** + * Line marker for running an introspection against a configured endpoint url in a .graphqlconfig file + */ +public class GraphQLIntrospectEndpointUrlLineMarkerProvider implements LineMarkerProvider { + @Nullable + @Override + public LineMarkerInfo getLineMarkerInfo(@NotNull PsiElement element) { + if (!GraphQLConfigManager.GRAPHQLCONFIG.equals(element.getContainingFile().getName())) { + return null; + } + if (element instanceof JsonProperty) { + final JsonProperty jsonProperty = (JsonProperty) element; + if ("url".equals(jsonProperty.getName()) && jsonProperty.getValue() instanceof JsonStringLiteral) { + + return new LineMarkerInfo<>((JsonStringLiteral) jsonProperty.getValue(), jsonProperty.getValue().getTextRange(), AllIcons.General.Run, Pass.UPDATE_ALL, o -> "Run introspection query to generate GraphQL SDL schema file", (evt, jsonUrl) -> { + + final String url = jsonUrl.getValue(); + + final GraphQLConfigVariableAwareEndpoint endpoint = getEndpoint(url, jsonProperty); + if (endpoint == null) { + return; + } + + String schemaPath = getSchemaPath(jsonProperty); + if (schemaPath == null || schemaPath.trim().isEmpty()) { + return; + } + + final HttpClient httpClient = new HttpClient(new HttpClientParams()); + + try { + + String query = GraphQLSettings.getSettings(element.getProject()).getIntrospectionQuery(); + if (StringUtils.isBlank(query)) { + query = IntrospectionQuery.INTROSPECTION_QUERY; + } + + final String requestJson = "{\"query\":\"" + StringEscapeUtils.escapeJavaScript(query) + "\"}"; + + final PostMethod method = new PostMethod(endpoint.getUrl()); + method.setRequestEntity(new StringRequestEntity(requestJson, "application/json", "UTF-8")); + + setHeadersFromOptions(endpoint, method); + + ProgressManager.getInstance().runProcessWithProgressSynchronously(() -> { + ProgressManager.getInstance().getProgressIndicator().setIndeterminate(true); + try { + httpClient.executeMethod(method); + final String responseJson = Optional.ofNullable(method.getResponseBodyAsString()).orElse(""); + ApplicationManager.getApplication().invokeLater(() -> { + try { + JSGraphQLLanguageUIProjectService.getService(jsonProperty.getProject()).showQueryResult(responseJson); + final String schemaAsSDL = printIntrospectionJsonAsGraphQL(responseJson); + VirtualFile virtualFile = element.getContainingFile().getVirtualFile(); + GraphQLIntrospectionHelper.createOrUpdateIntrospectionSDLFile(schemaAsSDL, virtualFile, schemaPath, element.getProject()); + } catch (Exception e) { + Notifications.Bus.notify(new Notification("GraphQL", "GraphQL Introspection Error", e.getMessage(), NotificationType.WARNING), element.getProject()); + } + }); + } catch (IOException e) { + Notifications.Bus.notify(new Notification("GraphQL", "GraphQL Query Error", url + ": " + e.getMessage(), NotificationType.WARNING), element.getProject()); + } + + }, "Executing GraphQL Introspection Query", false, jsonProperty.getProject()); + + + } catch (UnsupportedEncodingException | IllegalStateException | IllegalArgumentException e) { + Notifications.Bus.notify(new Notification("GraphQL", "GraphQL Query Error", url + ": " + e.getMessage(), NotificationType.ERROR), element.getProject()); + } + + }, GutterIconRenderer.Alignment.CENTER); + } + } + return null; + } + + private String getSchemaPath(JsonProperty urlElement) { + JsonObject jsonObject = PsiTreeUtil.getParentOfType(urlElement, JsonObject.class); + String url = urlElement.getValue() instanceof JsonStringLiteral ? ((JsonStringLiteral) urlElement.getValue()).getValue() : ""; + while (jsonObject != null) { + JsonProperty schemaPathElement = jsonObject.findProperty("schemaPath"); + if (schemaPathElement != null) { + if (schemaPathElement.getValue() instanceof JsonStringLiteral) { + String schemaPath = ((JsonStringLiteral) schemaPathElement.getValue()).getValue(); + if (schemaPath.trim().isEmpty()) { + Notifications.Bus.notify(new Notification("GraphQL", "GraphQL Configuration Error", "The schemaPath must be defined for url " + url, NotificationType.ERROR), urlElement.getProject()); + } + return schemaPath; + } else { + break; + } + } + jsonObject = PsiTreeUtil.getParentOfType(jsonObject, JsonObject.class); + } + Notifications.Bus.notify(new Notification("GraphQL", "GraphQL Configuration Error", "No schemaPath found for url " + url, NotificationType.ERROR), urlElement.getProject()); + return null; + } + + private GraphQLConfigVariableAwareEndpoint getEndpoint(String url, JsonProperty urlJsonProperty) { + try { + + final GraphQLConfigEndpoint endpointConfig = new GraphQLConfigEndpoint("", "", url); + + final Stream jsonPropertyStream = PsiTreeUtil.getChildrenOfTypeAsList(urlJsonProperty.getParent(), JsonProperty.class).stream(); + final Optional headers = jsonPropertyStream.filter(p -> "headers".equals(p.getName())).findFirst(); + headers.ifPresent(headersProp -> { + final JsonValue jsonValue = headersProp.getValue(); + if (jsonValue != null) { + endpointConfig.headers = new Gson().>fromJson(jsonValue.getText(), Map.class); + } + }); + + return new GraphQLConfigVariableAwareEndpoint(endpointConfig, urlJsonProperty.getProject()); + + } catch (Exception e) { + Notifications.Bus.notify(new Notification("GraphQL", "GraphQL Configuration Error", e.getMessage(), NotificationType.ERROR), urlJsonProperty.getProject()); + } + return null; + } + + @Override + public void collectSlowLineMarkers(@NotNull List elements, @NotNull Collection result) { + + } +} diff --git a/src/main/com/intellij/lang/jsgraphql/ide/editor/GraphQLIntrospectionHelper.java b/src/main/com/intellij/lang/jsgraphql/ide/editor/GraphQLIntrospectionHelper.java new file mode 100644 index 00000000..1cabd283 --- /dev/null +++ b/src/main/com/intellij/lang/jsgraphql/ide/editor/GraphQLIntrospectionHelper.java @@ -0,0 +1,96 @@ +/* + * Copyright (c) 2018-present, Jim Kynde Meyer + * All rights reserved. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ +package com.intellij.lang.jsgraphql.ide.editor; + +import com.google.gson.Gson; +import com.intellij.ide.actions.CreateFileAction; +import com.intellij.ide.impl.DataManagerImpl; +import com.intellij.notification.Notification; +import com.intellij.notification.NotificationType; +import com.intellij.notification.Notifications; +import com.intellij.openapi.actionSystem.ActionManager; +import com.intellij.openapi.actionSystem.ActionPlaces; +import com.intellij.openapi.actionSystem.AnAction; +import com.intellij.openapi.actionSystem.AnActionEvent; +import com.intellij.openapi.application.ApplicationManager; +import com.intellij.openapi.editor.Editor; +import com.intellij.openapi.fileEditor.FileEditor; +import com.intellij.openapi.fileEditor.FileEditorManager; +import com.intellij.openapi.fileEditor.TextEditor; +import com.intellij.openapi.project.Project; +import com.intellij.openapi.vfs.VirtualFile; +import com.intellij.psi.PsiDirectory; +import com.intellij.psi.impl.file.PsiDirectoryFactory; +import graphql.language.Document; +import graphql.schema.idl.SchemaPrinter; +import org.apache.commons.lang.StringUtils; + +import java.io.IOException; +import java.util.Date; +import java.util.Map; + +public class GraphQLIntrospectionHelper { + + @SuppressWarnings("unchecked") + static String printIntrospectionJsonAsGraphQL(String introspectionJson) { + Map introspectionAsMap = new Gson().fromJson(introspectionJson, Map.class); + if (!introspectionAsMap.containsKey("__schema")) { + // possibly a full query result + if (introspectionAsMap.containsKey("errors")) { + throw new IllegalArgumentException("Introspection query returned errors: " + new Gson().toJson(introspectionAsMap.get("errors"))); + } + if (!introspectionAsMap.containsKey("data")) { + throw new IllegalArgumentException("Expected data key to be present in query result. Got keys: " + introspectionAsMap.keySet()); + } + introspectionAsMap = (Map) introspectionAsMap.get("data"); + if (!introspectionAsMap.containsKey("__schema")) { + throw new IllegalArgumentException("Expected __schema key to be present in query result data. Got keys: " + introspectionAsMap.keySet()); + } + } + final Document schemaDefinition = new GraphQLIntrospectionResultToSchema().createSchemaDefinition(introspectionAsMap); + return new SchemaPrinter().print(schemaDefinition); + } + + + static void createOrUpdateIntrospectionSDLFile(String schemaAsSDL, VirtualFile introspectionSourceFile, String outputFileName, Project project) { + ApplicationManager.getApplication().runWriteAction(() -> { + try { + final String schemaAsSDLWithHeader = "# This file was generated based on \"" + introspectionSourceFile.getName() + "\" at " + new Date() + ". Do not edit manually.\n\n" + schemaAsSDL; + String relativeOutputFileName = StringUtils.replaceChars(outputFileName, '\\', '/'); + VirtualFile outputFile = introspectionSourceFile.getParent().findFileByRelativePath(relativeOutputFileName); + if (outputFile == null) { + PsiDirectory directory = PsiDirectoryFactory.getInstance(project).createDirectory(introspectionSourceFile.getParent()); + CreateFileAction.MkDirs dirs = new CreateFileAction.MkDirs(relativeOutputFileName, directory); + outputFile = dirs.directory.getVirtualFile().createChildData(introspectionSourceFile, dirs.newName); + } + final FileEditor fileEditor = FileEditorManager.getInstance(project).openFile(outputFile, true, true)[0]; + setEditorTextAndFormatLines(schemaAsSDLWithHeader, fileEditor); + } catch (IOException ioe) { + Notifications.Bus.notify(new Notification("GraphQL", "GraphQL IO Error", "Unable to create file '" + outputFileName + "' in directory '" + introspectionSourceFile.getParent().getPath() + "': " + ioe.getMessage(), NotificationType.ERROR)); + } + }); + } + + static void setEditorTextAndFormatLines(String text, FileEditor fileEditor) { + if (fileEditor instanceof TextEditor) { + final Editor editor = ((TextEditor) fileEditor).getEditor(); + editor.getDocument().setText(text); + AnAction reformatCode = ActionManager.getInstance().getAction("ReformatCode"); + if (reformatCode != null) { + final AnActionEvent actionEvent = AnActionEvent.createFromDataContext( + ActionPlaces.UNKNOWN, + null, + new DataManagerImpl.MyDataContext(editor.getComponent()) + ); + reformatCode.actionPerformed(actionEvent); + } + + } + } + +} diff --git a/src/main/com/intellij/lang/jsgraphql/ide/editor/GraphQLIntrospectionJsonToSDLLineMarkerProvider.java b/src/main/com/intellij/lang/jsgraphql/ide/editor/GraphQLIntrospectionJsonToSDLLineMarkerProvider.java new file mode 100644 index 00000000..0da12f37 --- /dev/null +++ b/src/main/com/intellij/lang/jsgraphql/ide/editor/GraphQLIntrospectionJsonToSDLLineMarkerProvider.java @@ -0,0 +1,78 @@ +/* + * Copyright (c) 2018-present, Jim Kynde Meyer + * All rights reserved. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ +package com.intellij.lang.jsgraphql.ide.editor; + +import com.intellij.codeHighlighting.Pass; +import com.intellij.codeInsight.daemon.LineMarkerInfo; +import com.intellij.codeInsight.daemon.LineMarkerProvider; +import com.intellij.icons.AllIcons; +import com.intellij.json.psi.JsonArray; +import com.intellij.json.psi.JsonObject; +import com.intellij.json.psi.JsonProperty; +import com.intellij.notification.Notification; +import com.intellij.notification.NotificationType; +import com.intellij.notification.Notifications; +import com.intellij.openapi.editor.markup.GutterIconRenderer; +import com.intellij.openapi.project.Project; +import com.intellij.openapi.vfs.VirtualFile; +import com.intellij.psi.PsiElement; +import com.intellij.psi.util.PsiTreeUtil; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +import java.util.Collection; +import java.util.List; + +import static com.intellij.lang.jsgraphql.ide.editor.GraphQLIntrospectionHelper.createOrUpdateIntrospectionSDLFile; +import static com.intellij.lang.jsgraphql.ide.editor.GraphQLIntrospectionHelper.printIntrospectionJsonAsGraphQL; + +/** + * Line marker which shows an action to turn a GraphQL Introspection JSON result into a GraphQL schema expressed in GraphQL SDL. + */ +public class GraphQLIntrospectionJsonToSDLLineMarkerProvider implements LineMarkerProvider { + @Nullable + @Override + @SuppressWarnings(value = "unchecked") + public LineMarkerInfo getLineMarkerInfo(@NotNull PsiElement element) { + if (element instanceof JsonProperty) { + if (PsiTreeUtil.getParentOfType(element, JsonProperty.class) == null) { + // top level property + final JsonProperty jsonProperty = (JsonProperty) element; + final String propertyName = jsonProperty.getName(); + if ("__schema".equals(propertyName) && jsonProperty.getValue() instanceof JsonObject) { + for (JsonProperty property : ((JsonObject) jsonProperty.getValue()).getPropertyList()) { + if ("types".equals(property.getName()) && property.getValue() instanceof JsonArray) { + // likely a GraphQL schema with a { __schema: { types: [] } } + return new LineMarkerInfo<>(jsonProperty, jsonProperty.getTextRange(), AllIcons.General.Run, Pass.UPDATE_ALL, o -> "Generate GraphQL SDL schema file", (e, elt) -> { + try { + final String introspectionJson = element.getContainingFile().getText(); + final String schemaAsSDL = printIntrospectionJsonAsGraphQL(introspectionJson); + + final VirtualFile jsonFile = element.getContainingFile().getVirtualFile(); + final String outputFileName = jsonFile.getName() + ".graphql"; + final Project project = element.getProject(); + + createOrUpdateIntrospectionSDLFile(schemaAsSDL, jsonFile, outputFileName, project); + + } catch (Exception exception) { + Notifications.Bus.notify(new Notification("GraphQL", "Unable to create GraphQL SDL", exception.getMessage(), NotificationType.ERROR)); + } + }, GutterIconRenderer.Alignment.CENTER); + } + } + } + } + } + return null; + } + + @Override + public void collectSlowLineMarkers(@NotNull List elements, @NotNull Collection result) { + + } +} diff --git a/src/main/com/intellij/lang/jsgraphql/ide/editor/GraphQLIntrospectionResultToSchema.java b/src/main/com/intellij/lang/jsgraphql/ide/editor/GraphQLIntrospectionResultToSchema.java new file mode 100644 index 00000000..5cf44d52 --- /dev/null +++ b/src/main/com/intellij/lang/jsgraphql/ide/editor/GraphQLIntrospectionResultToSchema.java @@ -0,0 +1,295 @@ +package com.intellij.lang.jsgraphql.ide.editor; + +import graphql.PublicApi; +import graphql.language.Argument; +import graphql.language.AstValueHelper; +import graphql.language.Description; +import graphql.language.Directive; +import graphql.language.Document; +import graphql.language.EnumTypeDefinition; +import graphql.language.EnumValueDefinition; +import graphql.language.FieldDefinition; +import graphql.language.InputObjectTypeDefinition; +import graphql.language.InputValueDefinition; +import graphql.language.InterfaceTypeDefinition; +import graphql.language.ListType; +import graphql.language.NonNullType; +import graphql.language.ObjectTypeDefinition; +import graphql.language.OperationTypeDefinition; +import graphql.language.ScalarTypeDefinition; +import graphql.language.SchemaDefinition; +import graphql.language.SourceLocation; +import graphql.language.StringValue; +import graphql.language.Type; +import graphql.language.TypeDefinition; +import graphql.language.TypeName; +import graphql.language.UnionTypeDefinition; +import graphql.language.Value; +import graphql.schema.idl.ScalarInfo; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.Map; +import java.util.stream.Collectors; + +import static graphql.Assert.assertNotNull; +import static graphql.Assert.assertShouldNeverHappen; +import static graphql.Assert.assertTrue; + +@SuppressWarnings("unchecked") +@PublicApi +public class GraphQLIntrospectionResultToSchema { + + /** + * Returns a IDL Document that reprSesents the schema as defined by the introspection result map + * + * @param introspectionResult the result of an introspection query on a schema + * + * @return a IDL Document of the schema + */ + @SuppressWarnings("unchecked") + public Document createSchemaDefinition(Map introspectionResult) { + assertTrue(introspectionResult.get("__schema") != null, "__schema expected"); + Map schema = (Map) introspectionResult.get("__schema"); + + + Map queryType = (Map) schema.get("queryType"); + assertNotNull(queryType, "queryType expected"); + TypeName query = new TypeName((String) queryType.get("name")); + boolean nonDefaultQueryName = !"Query".equals(query.getName()); + + SchemaDefinition schemaDefinition = new SchemaDefinition(); + schemaDefinition.getOperationTypeDefinitions().add(new OperationTypeDefinition("query", query)); + + Map mutationType = (Map) schema.get("mutationType"); + boolean nonDefaultMutationName = false; + if (mutationType != null) { + TypeName mutation = new TypeName((String) mutationType.get("name")); + nonDefaultMutationName = !"Mutation".equals(mutation.getName()); + schemaDefinition.getOperationTypeDefinitions().add(new OperationTypeDefinition("mutation", mutation)); + } + + Map subscriptionType = (Map) schema.get("subscriptionType"); + boolean nonDefaultSubscriptionName = false; + if (subscriptionType != null) { + TypeName subscription = new TypeName((String) subscriptionType.get("name")); + nonDefaultSubscriptionName = !"Subscription".equals(subscription.getName()); + schemaDefinition.getOperationTypeDefinitions().add(new OperationTypeDefinition("subscription", subscription)); + } + + Document document = new Document(); + if (nonDefaultQueryName || nonDefaultMutationName || nonDefaultSubscriptionName) { + document.getDefinitions().add(schemaDefinition); + } + + List> types = (List>) schema.get("types"); + for (Map type : types) { + TypeDefinition typeDefinition = createTypeDefinition(type); + if (typeDefinition == null) continue; + document.getDefinitions().add(typeDefinition); + } + + return document; + } + + private TypeDefinition createTypeDefinition(Map type) { + String kind = (String) type.get("kind"); + String name = (String) type.get("name"); + if (name.startsWith("__")) return null; + switch (kind) { + case "INTERFACE": + return createInterface(type); + case "OBJECT": + return createObject(type); + case "UNION": + return createUnion(type); + case "ENUM": + return createEnum(type); + case "INPUT_OBJECT": + return createInputObject(type); + case "SCALAR": + return createScalar(type); + default: + return assertShouldNeverHappen("unexpected kind %s", kind); + } + } + + private TypeDefinition createScalar(Map input) { + String name = (String) input.get("name"); + if (ScalarInfo.isStandardScalar(name)) { + return null; + } + return new ScalarTypeDefinition(name); + } + + + @SuppressWarnings("unchecked") + UnionTypeDefinition createUnion(Map input) { + assertTrue(input.get("kind").equals("UNION"), "wrong input"); + + UnionTypeDefinition unionTypeDefinition = new UnionTypeDefinition((String) input.get("name")); + unionTypeDefinition.setDescription(getDescription(input)); + + List> possibleTypes = (List>) input.get("possibleTypes"); + + for (Map possibleType : possibleTypes) { + TypeName typeName = new TypeName((String) possibleType.get("name")); + unionTypeDefinition.getMemberTypes().add(typeName); + } + + return unionTypeDefinition; + } + + @SuppressWarnings("unchecked") + EnumTypeDefinition createEnum(Map input) { + assertTrue(input.get("kind").equals("ENUM"), "wrong input"); + + EnumTypeDefinition enumTypeDefinition = new EnumTypeDefinition((String) input.get("name")); + enumTypeDefinition.setDescription(getDescription(input)); + + List> enumValues = (List>) input.get("enumValues"); + + for (Map enumValue : enumValues) { + + EnumValueDefinition enumValueDefinition = new EnumValueDefinition((String) enumValue.get("name")); + enumValueDefinition.setDescription(getDescription(enumValue)); + + createDeprecatedDirective(enumValue, enumValueDefinition.getDirectives()); + + enumTypeDefinition.getEnumValueDefinitions().add(enumValueDefinition); + } + + return enumTypeDefinition; + } + + @SuppressWarnings("unchecked") + InterfaceTypeDefinition createInterface(Map input) { + assertTrue(input.get("kind").equals("INTERFACE"), "wrong input"); + + InterfaceTypeDefinition interfaceTypeDefinition = new InterfaceTypeDefinition((String) input.get("name")); + interfaceTypeDefinition.setDescription(getDescription(input)); + List> fields = (List>) input.get("fields"); + interfaceTypeDefinition.getFieldDefinitions().addAll(createFields(fields)); + + return interfaceTypeDefinition; + + } + + @SuppressWarnings("unchecked") + InputObjectTypeDefinition createInputObject(Map input) { + assertTrue(input.get("kind").equals("INPUT_OBJECT"), "wrong input"); + + InputObjectTypeDefinition inputObjectTypeDefinition = new InputObjectTypeDefinition((String) input.get("name")); + inputObjectTypeDefinition.setDescription(getDescription(input)); + List> fields = (List>) input.get("inputFields"); + List inputValueDefinitions = createInputValueDefinitions(fields); + inputObjectTypeDefinition.getInputValueDefinitions().addAll(inputValueDefinitions); + + return inputObjectTypeDefinition; + } + + @SuppressWarnings("unchecked") + ObjectTypeDefinition createObject(Map input) { + assertTrue(input.get("kind").equals("OBJECT"), "wrong input"); + + ObjectTypeDefinition objectTypeDefinition = new ObjectTypeDefinition((String) input.get("name")); + objectTypeDefinition.setDescription(getDescription(input)); + if (input.containsKey("interfaces")) { + objectTypeDefinition.getImplements().addAll( + ((List>) input.get("interfaces")).stream() + .map(this::createTypeIndirection) + .collect(Collectors.toList()) + ); + } + List> fields = (List>) input.get("fields"); + + objectTypeDefinition.getFieldDefinitions().addAll(createFields(fields)); + + return objectTypeDefinition; + } + + private List createFields(List> fields) { + List result = new ArrayList<>(); + for (Map field : fields) { + FieldDefinition fieldDefinition = new FieldDefinition((String) field.get("name")); + fieldDefinition.setDescription(getDescription(field)); + fieldDefinition.setType(createTypeIndirection((Map) field.get("type"))); + + createDeprecatedDirective(field, fieldDefinition.getDirectives()); + + List> args = (List>) field.get("args"); + List inputValueDefinitions = createInputValueDefinitions(args); + fieldDefinition.getInputValueDefinitions().addAll(inputValueDefinitions); + result.add(fieldDefinition); + } + return result; + } + + private void createDeprecatedDirective(Map field, List directives) { + if ((Boolean) field.get("isDeprecated")) { + String reason = (String) field.get("deprecationReason"); + if (reason == null) { + reason = "No longer supported"; // default according to spec + } + Argument reasonArg = new Argument("reason", new StringValue(reason)); + Directive deprecated = new Directive("deprecated", Collections.singletonList(reasonArg)); + directives.add(deprecated); + } + } + + @SuppressWarnings("unchecked") + private List createInputValueDefinitions(List> args) { + List result = new ArrayList<>(); + for (Map arg : args) { + Type argType = createTypeIndirection((Map) arg.get("type")); + InputValueDefinition inputValueDefinition = new InputValueDefinition((String) arg.get("name"), argType); + inputValueDefinition.setDescription(getDescription(arg)); + + String valueLiteral = (String) arg.get("defaultValue"); + if (valueLiteral != null) { + Value defaultValue = AstValueHelper.valueFromAst(valueLiteral); + inputValueDefinition.setDefaultValue(defaultValue); + } + result.add(inputValueDefinition); + } + return result; + } + + @SuppressWarnings("unchecked") + private Type createTypeIndirection(Map type) { + String kind = (String) type.get("kind"); + switch (kind) { + case "INTERFACE": + case "OBJECT": + case "UNION": + case "ENUM": + case "INPUT_OBJECT": + case "SCALAR": + return new TypeName((String) type.get("name")); + case "NON_NULL": + return new NonNullType(createTypeIndirection((Map) type.get("ofType"))); + case "LIST": + return new ListType(createTypeIndirection((Map) type.get("ofType"))); + default: + return assertShouldNeverHappen("Unknown kind %s", kind); + } + } + + private Description getDescription(Map descriptionAware) { + final Object rawDescription = descriptionAware.get("description"); + if (rawDescription instanceof String) { + String description = (String) rawDescription; + if (!description.trim().isEmpty()) { + final boolean multiLine = description.contains("\n"); + if (multiLine) { + // ensures the description stands on separate lines from the triple quotes + description = "\n" + description.trim() + "\n"; + } + return new Description(description, new SourceLocation(1, 1), multiLine); + } + } + return null; + } + +} diff --git a/src/main/com/intellij/lang/jsgraphql/ide/project/GraphQLIntrospectionProjectViewNestingRulesProvider.java b/src/main/com/intellij/lang/jsgraphql/ide/project/GraphQLIntrospectionProjectViewNestingRulesProvider.java new file mode 100644 index 00000000..aa321f4d --- /dev/null +++ b/src/main/com/intellij/lang/jsgraphql/ide/project/GraphQLIntrospectionProjectViewNestingRulesProvider.java @@ -0,0 +1,22 @@ +/* + * Copyright (c) 2018-present, Jim Kynde Meyer + * All rights reserved. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ +package com.intellij.lang.jsgraphql.ide.project; + +import com.intellij.ide.projectView.ProjectViewNestingRulesProvider; +import org.jetbrains.annotations.NotNull; + +/** + * Nests GraphQL files created using introspection under their source JSON files. + */ +public class GraphQLIntrospectionProjectViewNestingRulesProvider implements ProjectViewNestingRulesProvider { + + @Override + public void addFileNestingRules(@NotNull Consumer consumer) { + consumer.addNestingRule(".json", ".json.graphql"); + } +} diff --git a/src/main/com/intellij/lang/jsgraphql/ide/project/graphqlconfig/GraphQLConfigPackageSet.java b/src/main/com/intellij/lang/jsgraphql/ide/project/graphqlconfig/GraphQLConfigPackageSet.java index d6231b79..050904a4 100644 --- a/src/main/com/intellij/lang/jsgraphql/ide/project/graphqlconfig/GraphQLConfigPackageSet.java +++ b/src/main/com/intellij/lang/jsgraphql/ide/project/graphqlconfig/GraphQLConfigPackageSet.java @@ -45,7 +45,7 @@ class GraphQLConfigPackageSet implements PackageSet { this.globMatcher = globMatcher; if (StringUtils.isNotEmpty(configData.schemaPath)) { - VirtualFile schemaFile = configBaseDir.findFileByRelativePath(configData.schemaPath); + VirtualFile schemaFile = configBaseDir.findFileByRelativePath(StringUtils.replaceChars(configData.schemaPath, '\\', '/')); if (schemaFile != null) { schemaFilePath = schemaFile.getPath(); } diff --git a/src/main/com/intellij/lang/jsgraphql/ide/project/graphqlconfig/model/GraphQLConfigVariableAwareEndpoint.java b/src/main/com/intellij/lang/jsgraphql/ide/project/graphqlconfig/model/GraphQLConfigVariableAwareEndpoint.java index f65a0435..4d1b58b1 100644 --- a/src/main/com/intellij/lang/jsgraphql/ide/project/graphqlconfig/model/GraphQLConfigVariableAwareEndpoint.java +++ b/src/main/com/intellij/lang/jsgraphql/ide/project/graphqlconfig/model/GraphQLConfigVariableAwareEndpoint.java @@ -69,7 +69,7 @@ private Map expandVariables(Map map) { return map; } - private String expandVariables(String rawValue) { + public String expandVariables(String rawValue) { Matcher matcher = ENV_PATTERN.matcher(rawValue); diff --git a/src/main/com/intellij/lang/jsgraphql/ui/GraphQLProjectSettingsForm.form b/src/main/com/intellij/lang/jsgraphql/ui/GraphQLProjectSettingsForm.form index 63e2284e..ac81fedd 100644 --- a/src/main/com/intellij/lang/jsgraphql/ui/GraphQLProjectSettingsForm.form +++ b/src/main/com/intellij/lang/jsgraphql/ui/GraphQLProjectSettingsForm.form @@ -123,7 +123,7 @@ - + @@ -133,7 +133,7 @@ - + diff --git a/src/main/com/intellij/lang/jsgraphql/ui/GraphQLProjectSettingsForm.java b/src/main/com/intellij/lang/jsgraphql/ui/GraphQLProjectSettingsForm.java index e3ee8e8b..1aee266e 100644 --- a/src/main/com/intellij/lang/jsgraphql/ui/GraphQLProjectSettingsForm.java +++ b/src/main/com/intellij/lang/jsgraphql/ui/GraphQLProjectSettingsForm.java @@ -24,6 +24,7 @@ import java.lang.reflect.InvocationTargetException; import java.lang.reflect.Method; import java.util.Map; +import java.util.Objects; import java.util.function.Predicate; public class GraphQLProjectSettingsForm { @@ -58,6 +59,7 @@ GraphQLProjectSettingsForm initialize(GraphQLSettings mySettings) { schemasPanel.setBorder(IdeBorderFactory.createTitledBorder("GraphQL Project Structure and Schemas")); introspectionPanel.setBorder(IdeBorderFactory.createTitledBorder("GraphQL Introspection")); + automaticallyUpdateGraphQLFilesCheckBox.setVisible(false); final HoverHyperlinkLabel editScopesLink = new HoverHyperlinkLabel("Edit scopes"); editScopesLink.addHyperlinkListener(hyperlinkEvent -> { @@ -122,14 +124,16 @@ void apply() throws ConfigurationException { mySettings.setScopeResolution(graphQLScopeResolution); } }); + mySettings.setIntrospectionQuery(introspectionQueryTextField.getText()); } void reset() { scopes.get(mySettings.getScopeResolution()).setSelected(true); + introspectionQueryTextField.setText(mySettings.getIntrospectionQuery()); } boolean isModified() { - return !scopes.get(mySettings.getScopeResolution()).isSelected(); + return !scopes.get(mySettings.getScopeResolution()).isSelected() || !Objects.equals(mySettings.getIntrospectionQuery(), introspectionQueryTextField.getText()); } } diff --git a/src/main/com/intellij/lang/jsgraphql/v1/ide/project/JSGraphQLLanguageUIProjectService.java b/src/main/com/intellij/lang/jsgraphql/v1/ide/project/JSGraphQLLanguageUIProjectService.java index c5017b4b..d7e1bc2e 100644 --- a/src/main/com/intellij/lang/jsgraphql/v1/ide/project/JSGraphQLLanguageUIProjectService.java +++ b/src/main/com/intellij/lang/jsgraphql/v1/ide/project/JSGraphQLLanguageUIProjectService.java @@ -436,16 +436,7 @@ public void executeGraphQL(Editor editor, VirtualFile virtualFile) { if (fileEditor instanceof TextEditor) { final TextEditor textEditor = (TextEditor) fileEditor; UIUtil.invokeLaterIfNeeded(() -> { - ApplicationManager.getApplication().runWriteAction(() -> { - final Document document = textEditor.getEditor().getDocument(); - document.setText(responseJson); - if(requestJson.startsWith("{")) { - final PsiFile psiFile = PsiDocumentManager.getInstance(myProject).getPsiFile(document); - if (psiFile != null) { - new ReformatCodeProcessor(psiFile, false).run(); - } - } - }); + updateQueryResultEditor(responseJson, textEditor); final StringBuilder queryResultText = new StringBuilder(virtualFile.getName()). append(": "). append(sw.getTime()). @@ -476,14 +467,13 @@ public void executeGraphQL(Editor editor, VirtualFile virtualFile) { querySuccessLabel.setIcon(AllIcons.Ide.ErrorPoint); } } - showToolWindowContent(myProject, fileEditor.getComponent().getClass()); - textEditor.getEditor().getScrollingModel().scrollVertically(0); + showQueryResultEditor(textEditor); }); } } finally { editor.putUserData(JS_GRAPH_QL_EDITOR_QUERYING, null); } - } catch (IOException e) { + } catch (IOException | IllegalArgumentException e) { Notifications.Bus.notify(new Notification("GraphQL", "GraphQL Query Error", url + ": " + e.getMessage(), NotificationType.WARNING), myProject); } }); @@ -495,6 +485,32 @@ public void executeGraphQL(Editor editor, VirtualFile virtualFile) { } } + public void showQueryResult(String jsonResponse) { + if(fileEditor instanceof TextEditor) { + final TextEditor fileEditor = (TextEditor) this.fileEditor; + updateQueryResultEditor(jsonResponse, fileEditor); + showQueryResultEditor(fileEditor); + } + } + + private void showQueryResultEditor(TextEditor textEditor) { + showToolWindowContent(myProject, fileEditor.getComponent().getClass()); + textEditor.getEditor().getScrollingModel().scrollVertically(0); + } + + private void updateQueryResultEditor(String responseJson, TextEditor textEditor) { + ApplicationManager.getApplication().runWriteAction(() -> { + final Document document = textEditor.getEditor().getDocument(); + document.setText(responseJson); + if(responseJson.startsWith("{")) { + final PsiFile psiFile = PsiDocumentManager.getInstance(myProject).getPsiFile(document); + if (psiFile != null) { + new ReformatCodeProcessor(psiFile, false).run(); + } + } + }); + } + private Integer getErrorCount(String responseJson) { try { final Map res = new Gson().fromJson(responseJson, Map.class); @@ -529,7 +545,7 @@ private static String bytesToDisplayString(long bytes) { return String.format("%.1f %sb", bytes / Math.pow(1000, exp), pre); } - private void setHeadersFromOptions(GraphQLConfigVariableAwareEndpoint endpoint, PostMethod method) { + public static void setHeadersFromOptions(GraphQLConfigVariableAwareEndpoint endpoint, PostMethod method) { final Map headers = endpoint.getHeaders(); if (headers != null) { for (Map.Entry entry : headers.entrySet()) {