Skip to content

Commit

Permalink
Added tools for introspection (#164)
Browse files Browse the repository at this point in the history
- Introspection query execute line marker on url in .graphqlconfig
- Print schema JSON as SDL line marker
  • Loading branch information
jimkyndemeyer committed Jul 1, 2018
1 parent 771636d commit 1e92316
Show file tree
Hide file tree
Showing 12 changed files with 724 additions and 20 deletions.
6 changes: 6 additions & 0 deletions resources/META-INF/plugin.xml
Original file line number Diff line number Diff line change
Expand Up @@ -102,6 +102,12 @@
<!-- Editor notifications -->
<editorNotificationProvider implementation="com.intellij.lang.jsgraphql.ide.notifications.GraphQLScopeEditorNotificationProvider"/>

<!-- Introspection -->
<codeInsight.lineMarkerProvider implementationClass="com.intellij.lang.jsgraphql.ide.editor.GraphQLIntrospectionJsonToSDLLineMarkerProvider" language="JSON" />
<codeInsight.lineMarkerProvider implementationClass="com.intellij.lang.jsgraphql.ide.editor.GraphQLIntrospectEndpointUrlLineMarkerProvider" language="JSON" />
<projectViewNestingRulesProvider implementation="com.intellij.lang.jsgraphql.ide.project.GraphQLIntrospectionProjectViewNestingRulesProvider" />


<!-- v2 above this point -->


Expand Down
11 changes: 10 additions & 1 deletion src/main/com/intellij/lang/jsgraphql/GraphQLSettings.java
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}

/**
Expand All @@ -53,6 +61,7 @@ public void setScopeResolution(GraphQLScopeResolution scopeResolution) {
*/
static class GraphQLSettingsState {
public GraphQLScopeResolution scopeResolution = GraphQLScopeResolution.ENTIRE_PROJECT;
public String introspectionQuery = "";
}
}

Original file line number Diff line number Diff line change
@@ -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<JsonProperty> jsonPropertyStream = PsiTreeUtil.getChildrenOfTypeAsList(urlJsonProperty.getParent(), JsonProperty.class).stream();
final Optional<JsonProperty> headers = jsonPropertyStream.filter(p -> "headers".equals(p.getName())).findFirst();
headers.ifPresent(headersProp -> {
final JsonValue jsonValue = headersProp.getValue();
if (jsonValue != null) {
endpointConfig.headers = new Gson().<Map<String, Object>>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<PsiElement> elements, @NotNull Collection<LineMarkerInfo> result) {

}
}
Original file line number Diff line number Diff line change
@@ -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<String, Object> 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<String, Object>) 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);
}

}
}

}
Original file line number Diff line number Diff line change
@@ -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<PsiElement> elements, @NotNull Collection<LineMarkerInfo> result) {

}
}
Loading

0 comments on commit 1e92316

Please sign in to comment.