-
Notifications
You must be signed in to change notification settings - Fork 95
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Added tools for introspection (#164)
- Introspection query execute line marker on url in .graphqlconfig - Print schema JSON as SDL line marker
- Loading branch information
1 parent
771636d
commit 1e92316
Showing
12 changed files
with
724 additions
and
20 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
178 changes: 178 additions & 0 deletions
178
...om/intellij/lang/jsgraphql/ide/editor/GraphQLIntrospectEndpointUrlLineMarkerProvider.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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) { | ||
|
||
} | ||
} |
96 changes: 96 additions & 0 deletions
96
src/main/com/intellij/lang/jsgraphql/ide/editor/GraphQLIntrospectionHelper.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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); | ||
} | ||
|
||
} | ||
} | ||
|
||
} |
78 changes: 78 additions & 0 deletions
78
...m/intellij/lang/jsgraphql/ide/editor/GraphQLIntrospectionJsonToSDLLineMarkerProvider.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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) { | ||
|
||
} | ||
} |
Oops, something went wrong.