Skip to content
This repository has been archived by the owner on Feb 1, 2023. It is now read-only.

Commit

Permalink
feature: preview of AsyncAPI schema as html in built-in/external browser
Browse files Browse the repository at this point in the history
Initial commit. We can preview AsyncAPI schema as html in built-in/external browser. Known limitations: reload on save doesn't work

Feature was implemented early because of feature request. See GitHub issue.
#3
  • Loading branch information
Pakisan committed Sep 30, 2021
1 parent f3d4347 commit a138635
Show file tree
Hide file tree
Showing 5 changed files with 314 additions and 0 deletions.
111 changes: 111 additions & 0 deletions src/main/kotlin/com/asyncapi/plugin/idea/_core/SchemaHtmlRenderer.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,111 @@
package com.asyncapi.plugin.idea._core

import com.fasterxml.jackson.databind.ObjectMapper
import com.fasterxml.jackson.databind.node.ObjectNode
import com.fasterxml.jackson.dataformat.yaml.YAMLFactory
import com.intellij.json.JsonFileType
import com.intellij.openapi.util.io.FileUtil
import com.intellij.openapi.vfs.LocalFileSystem
import com.intellij.openapi.vfs.VirtualFile
import com.intellij.util.Urls
import org.jetbrains.ide.BuiltInServerManager
import org.jetbrains.yaml.YAMLFileType
import java.io.File

class SchemaHtmlRenderer {

private val schemaTemplateUrl = "/ui/index.html"
private val serverManager = BuiltInServerManager.getInstance()

fun render(schemaUrl: String?): String {
schemaUrl ?: return "schema: not found."

val schemaFile = File(schemaUrl)
if (!schemaFile.exists()) {
return "schema: $schemaUrl not found."
}

val schemaVirtualFile = LocalFileSystem.getInstance().findFileByIoFile(schemaFile)
schemaVirtualFile ?: return "schema: $schemaUrl not found."
if (schemaVirtualFile.fileType !is YAMLFileType && schemaVirtualFile.fileType !is JsonFileType) {
return "schema: $schemaUrl not in json or yaml format."
}

val isJson = schemaVirtualFile.fileType is JsonFileType
val schema = replaceLocalReferences(schemaFile.readText(Charsets.UTF_8), schemaVirtualFile, isJson)
val temporalSchemaUrl = saveAsTemporalFile(schema, isJson)

val schemaTemplate = this.javaClass.getResource(schemaTemplateUrl)
schemaTemplate ?: return "schema template not found."

return schemaTemplate.readText(Charsets.UTF_8)
// .replace(
// "schema: ``,",
// "schema: `${schema.removePrefix("\"").removeSuffix("\"")}`,"
// )
.replace(
"url: '',",
"url: '${urlToRequestedSchema(temporalSchemaUrl)}',"
)
}

private fun replaceLocalReferences(schema: String, schemaFile: VirtualFile, isJson: Boolean): String {
val objectMapper = if (isJson) {
ObjectMapper()
} else {
ObjectMapper(YAMLFactory())
}

val tree = objectMapper.readTree(schema)
tree.findParents("\$ref").forEach {
val referenceValue = (it as ObjectNode).get("\$ref")
if (referenceValue.toPrettyString().startsWith("\"./") || referenceValue.toPrettyString().startsWith("\"../")) {
(it).put("\$ref", localReferenceToFileUrl(referenceValue.toPrettyString(), schemaFile))
}
}

return objectMapper.writeValueAsString(tree)
}

private fun localReferenceToFileUrl(localReference: String, schemaFile: VirtualFile): String {
val rawFileReference = localReference.removePrefix("\"").removeSuffix("\"")
val fileReference = rawFileReference.split("#/").getOrNull(0)
val schemaReference = rawFileReference.split("#/").getOrNull(1)
fileReference ?: return rawFileReference

val referencedFile = schemaFile.parent.findFileByRelativePath(fileReference)
referencedFile ?: return fileReference

return urlToReferencedFile(referencedFile.path, schemaReference)
}

private fun urlToReferencedFile(fileUrl: String, schemaReference: String?): String {
val url = if (schemaReference != null) {
Urls.parseEncoded("http://localhost:${serverManager.port}/asyncapi/resources?referenceUrl=$fileUrl#/$schemaReference")
} else {
Urls.parseEncoded("http://localhost:${serverManager.port}/asyncapi/resources?referenceUrl=$fileUrl")
}

return serverManager.addAuthToken(url!!).toExternalForm()
}

private fun urlToRequestedSchema(schemaUrl: String): String {
val url = Urls.parseEncoded("http://localhost:${serverManager.port}/asyncapi/resources?schemaUrl=$schemaUrl")

return serverManager.addAuthToken(url!!).toExternalForm()
}

private fun saveAsTemporalFile(schema: String, isJson: Boolean): String {
val suffix = if (isJson) {
".json"
} else {
".yaml"
}

val tempSchema = FileUtil.createTempFile("jasyncapi-idea-plugin-${System.currentTimeMillis()}", suffix, true)
tempSchema.writeText(schema, Charsets.UTF_8)

return tempSchema.path
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
package com.asyncapi.plugin.idea.extensions.web

import com.asyncapi.plugin.idea.extensions.inspection.AsyncAPISchemaDetector
import com.intellij.ide.browsers.OpenInBrowserRequest
import com.intellij.ide.browsers.WebBrowserUrlProvider
import com.intellij.json.psi.JsonFile
import com.intellij.openapi.vfs.VirtualFile
import com.intellij.psi.PsiFile
import com.intellij.util.Url
import org.jetbrains.yaml.psi.YAMLFile

class AsyncAPIBrowserUrlProvider: WebBrowserUrlProvider() {

private val asyncApiSchemaDetector = AsyncAPISchemaDetector()
private val staticServer = StaticServer()

override fun canHandleElement(request: OpenInBrowserRequest): Boolean {
if (request.file is JsonFile || request.file is YAMLFile) {
if (asyncApiSchemaDetector.isAsyncAPISchema(request.file as? PsiFile)) {
return true
}
}

return false
}

override fun getUrl(request: OpenInBrowserRequest, file: VirtualFile): Url? {
return staticServer.getUrl(request, file)
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,144 @@
package com.asyncapi.plugin.idea.extensions.web

import com.asyncapi.plugin.idea._core.SchemaHtmlRenderer
import com.intellij.ide.browsers.OpenInBrowserRequest
import com.intellij.json.JsonFileType
import com.intellij.openapi.vfs.LocalFileSystem
import com.intellij.openapi.vfs.VirtualFile
import com.intellij.util.Url
import com.intellij.util.Urls
import io.netty.buffer.Unpooled
import io.netty.channel.ChannelHandlerContext
import io.netty.handler.codec.http.*
import org.jetbrains.ide.BuiltInServerManager
import org.jetbrains.ide.HttpRequestHandler
import org.jetbrains.io.send
import org.jetbrains.yaml.YAMLFileType
import java.io.File
import java.net.URLEncoder
import java.nio.charset.StandardCharsets
import java.util.*

class StaticServer : HttpRequestHandler() {

private val schemaHtmlRenderer = SchemaHtmlRenderer()

fun getUrl(request: OpenInBrowserRequest, file: VirtualFile): Url? {
val port = BuiltInServerManager.getInstance().port
val schemaUrl = URLEncoder.encode(file.path, StandardCharsets.UTF_8.toString())
val projectUrl = URLEncoder.encode(request.project.presentableUrl, StandardCharsets.UTF_8.toString())
val projectName = URLEncoder.encode(request.project.name, StandardCharsets.UTF_8.toString())
val address = "http://localhost:$port/asyncapi/render" +
"?schemaUrl=$schemaUrl" +
"&projectUrl=$projectUrl" +
"&projectName=$projectName" +
"&_ij_reload=RELOAD_ON_SAVE"

val url = Urls.parseEncoded(address)
// val url = Urls.parseEncoded(getStaticUrl())
return if (request.isAppendAccessToken) {
BuiltInServerManager.getInstance().addAuthToken(Objects.requireNonNull<Url>(url))
} else {
url
}
}

override fun isAccessible(request: HttpRequest): Boolean {
return request.uri().startsWith("/asyncapi") && super.isAccessible(request)
}

override fun isSupported(request: FullHttpRequest): Boolean {
return request.uri().startsWith("/asyncapi") && super.isAccessible(request)
}

override fun process(
urlDecoder: QueryStringDecoder,
request: FullHttpRequest,
context: ChannelHandlerContext
): Boolean {
val htmlPage = urlDecoder.path().startsWith("/asyncapi/render")
val reference = urlDecoder.path().startsWith("/asyncapi/resources") && urlDecoder.parameters().contains("referenceUrl")
val schema = urlDecoder.path().startsWith("/asyncapi/resources") && urlDecoder.parameters().contains("schemaUrl")

return if (htmlPage) {
val schemaUrl = if (urlDecoder.parameters().contains("schemaUrl")) {
urlDecoder.parameters()["schemaUrl"]?.get(0)
} else {
null
}

val html = schemaHtmlRenderer.render(schemaUrl).toByteArray(StandardCharsets.UTF_8)

sendResponse(html, request, context, "text/html")
true
} else if (reference) {
val referenceUrl = if (urlDecoder.parameters().contains("referenceUrl")) {
urlDecoder.parameters()["referenceUrl"]?.get(0)
} else {
null
}

referenceUrl ?: return false
val requestedFile = resolveResource(referenceUrl)
requestedFile ?: return false

sendResponse(requestedFile.second, request, context, requestedFile.first)
true
} else if (schema) {
val schemaUrl = if (urlDecoder.parameters().contains("schemaUrl")) {
urlDecoder.parameters()["schemaUrl"]?.get(0)
} else {
null
}

schemaUrl ?: return false
val requestedFile = resolveResource(schemaUrl)
requestedFile ?: return false

sendResponse(requestedFile.second, request, context, requestedFile.first)
true
} else {
false
}
}

private fun sendResponse(content: ByteArray,
request: FullHttpRequest,
context: ChannelHandlerContext,
contentType: String
) {
val response: FullHttpResponse = DefaultFullHttpResponse(
HttpVersion.HTTP_1_1,
HttpResponseStatus.OK,
Unpooled.wrappedBuffer(content)
)
response.headers()[HttpHeaderNames.CONTENT_TYPE] = "$contentType; charset=UTF-8"
response.headers()[HttpHeaderNames.CACHE_CONTROL] = "max-age=5, private, must-revalidate"
response.headers()["Referrer-Policy"] = "no-referrer"
response.send(context.channel(), request)
}

private fun resolveResource(resourceUrl: String): Pair<String, ByteArray>? {
val requestedFile = File(resourceUrl)
if (!requestedFile.exists()) {
return null
}

val referenceVirtualFile = LocalFileSystem.getInstance().findFileByIoFile(requestedFile)
referenceVirtualFile ?: return null

if (referenceVirtualFile.fileType !is YAMLFileType && referenceVirtualFile.fileType !is JsonFileType) {
return null
}

val isJson = referenceVirtualFile.fileType is JsonFileType
val contentType = if (isJson) {
"application/json"
} else {
"application/x-yaml"
}

return Pair(contentType, requestedFile.readBytes())
}

}
4 changes: 4 additions & 0 deletions src/main/resources/META-INF/plugin.xml
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,10 @@
<!-- AsyncAPI references -->
<psi.referenceContributor language="JSON" implementation="com.asyncapi.plugin.idea.extensions.psi.reference.contributor.json.AsyncAPISchemaReferenceContributor"/>
<psi.referenceContributor language="yaml" implementation="com.asyncapi.plugin.idea.extensions.psi.reference.contributor.yaml.AsyncAPISchemaReferenceContributor"/>

<!-- Preview in built-in/external browser -->
<webBrowserUrlProvider implementation="com.asyncapi.plugin.idea.extensions.web.AsyncAPIBrowserUrlProvider"/>
<httpRequestHandler implementation="com.asyncapi.plugin.idea.extensions.web.StaticServer"/>
</extensions>

<actions>
Expand Down
24 changes: 24 additions & 0 deletions src/main/resources/ui/index.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
<!DOCTYPE html>
<html>
<head>
<link rel="stylesheet" href="https://unpkg.com/@asyncapi/react-component@1.0.0-next.12/styles/default.min.css">
</head>
<body>
<div id="asyncapi"></div>

<script src="https://unpkg.com/@asyncapi/react-component@1.0.0-next.12/browser/standalone/index.js"></script>
<script>
AsyncApiStandalone.render({
schema: {
url: '',
options: { method: "GET", mode: "cors" },
},
config: {
show: {
sidebar: true,
}
},
}, document.getElementById('asyncapi'));
</script>
</body>
</html>

0 comments on commit a138635

Please sign in to comment.