Skip to content

Commit

Permalink
feat: Add support for exporting APIs as .http files
Browse files Browse the repository at this point in the history
  • Loading branch information
tangcent committed Nov 19, 2023
1 parent d759761 commit 77e6e10
Show file tree
Hide file tree
Showing 11 changed files with 2,172 additions and 17 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
package com.itangcent.idea.plugin.api.export.http

import com.google.inject.Inject
import com.google.inject.Singleton
import com.itangcent.cache.HttpContextCacheHelper
import com.itangcent.common.logger.traceError
import com.itangcent.common.model.Request
import com.itangcent.idea.plugin.api.export.core.FormatFolderHelper
import com.itangcent.idea.utils.ModuleHelper
import com.itangcent.intellij.logger.Logger

/**
* export requests as httpClient command
* @author tangcent
*/
@Singleton
class HttpClientExporter {

@Inject
private lateinit var httpClientFormatter: HttpClientFormatter

@Inject
private lateinit var httpClientFileSaver: HttpClientFileSaver

@Inject
private lateinit var logger: Logger

@Inject
private lateinit var moduleHelper: ModuleHelper

@Inject
private lateinit var formatFolderHelper: FormatFolderHelper

@Inject
private lateinit var httpContextCacheHelper: HttpContextCacheHelper

fun export(requests: List<Request>) {
try {
if (requests.isEmpty()) {
logger.info("No api be found to export!")
return
}
exportToFile(requests)
} catch (e: Exception) {
logger.traceError("Apis save failed", e)
}
}

private fun exportToFile(requests: List<Request>) {
// 1. Group requests by module and folder
val moduleFolderRequestMap = mutableMapOf<String, MutableMap<String, MutableList<Request>>>()

for (request in requests) {
val module = moduleHelper.findModule(request.resource!!) ?: "easy-yapi"
val folder = formatFolderHelper.resolveFolder(request.resource!!)
val folderRequestMap = moduleFolderRequestMap.getOrPut(module) { mutableMapOf() }
folderRequestMap.getOrPut(folder.name ?: "apis") { mutableListOf() }.add(request)
}

// 2. Process each module
for ((module, folderRequestMap) in moduleFolderRequestMap) {
// 3. Process each folder within the module
for ((folder, folderRequests) in folderRequestMap) {
val host = httpContextCacheHelper.selectHost("Select Host For $module")
httpClientFileSaver.saveAndOpenHttpFile(module, "$folder.http") { existedContent ->
if (existedContent == null) {
httpClientFormatter.parseRequests(
host = host, requests = folderRequests
)
} else {
httpClientFormatter.parseRequests(
existedDoc = existedContent,
host = host,
requests = folderRequests
)
}
}
}
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
package com.itangcent.idea.plugin.api.export.http

import com.google.inject.Inject
import com.google.inject.Singleton
import com.intellij.openapi.application.PathManager
import com.intellij.openapi.fileEditor.FileEditorManager
import com.intellij.openapi.project.Project
import com.intellij.openapi.vfs.LocalFileSystem
import com.intellij.openapi.vfs.VirtualFile
import com.intellij.util.io.createDirectories
import com.intellij.util.io.readText
import com.itangcent.intellij.context.ActionContext
import java.io.IOException
import java.nio.file.Files
import java.nio.file.Path
import java.nio.file.Paths
import kotlin.io.path.writeText

/**
* @author tangcent
*/
@Singleton
class HttpClientFileSaver {

@Inject
private lateinit var project: Project

@Inject
private lateinit var actionContext: ActionContext

private val scratchesPath: Path by lazy { Paths.get(PathManager.getConfigPath(), "scratches") }

private val localFileSystem by lazy { LocalFileSystem.getInstance() }

fun saveAndOpenHttpFile(
module: String,
fileName: String,
content: (String?) -> String,
) {
val file = saveHttpFile(module, fileName, content)
openInEditor(file)
}

private fun saveHttpFile(
module: String,
fileName: String,
content: (String?) -> String,
): VirtualFile {
val file = scratchesPath.resolve(module).resolve(fileName).apply {
parent.createDirectories()
}
file.writeText(content(file.takeIf { Files.exists(it) }?.readText()))

return (localFileSystem.refreshAndFindFileByPath(file.toString())
?: throw IOException("Unable to find file: $file"))
}

private fun openInEditor(file: VirtualFile) {
actionContext.runInWriteUI {
FileEditorManager.getInstance(project).openFile(file, true)
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,176 @@
package com.itangcent.idea.plugin.api.export.http

import com.google.inject.Inject
import com.google.inject.Singleton
import com.itangcent.common.model.Request
import com.itangcent.common.model.getContentType
import com.itangcent.common.utils.IDUtils
import com.itangcent.http.RequestUtils
import com.itangcent.idea.psi.resource
import com.itangcent.intellij.context.ActionContext
import com.itangcent.intellij.psi.PsiClassUtils

/**
* @author tangcent
*/
@Singleton
class HttpClientFormatter {

@Inject
private lateinit var actionContext: ActionContext

companion object {
const val REF = "### ref: "
}

fun parseRequests(
host: String,
requests: List<Request>
): String {
val sb = StringBuilder()
for (request in requests) {
parseRequest(host, request, sb)
}
return sb.toString()
}

fun parseRequests(
existedDoc: String,
host: String,
requests: List<Request>
): String {
val docs = splitDoc(existedDoc)
val requestMap = requests.associateBy {
it.ref()
}
val sb = StringBuilder()

// Process and update existing entries
for (doc in docs) {
val request = requestMap[doc.first]
if (request != null) {
parseRequest(host, request, sb)
} else {
sb.append(REF).append(doc.first).append("\n")
.append(doc.second)
}
}

// Process new requests
val processedRefs = docs.map { it.first }.toSet()
requests.filter { request ->
request.ref() !in processedRefs
}.forEach { request ->
parseRequest(host, request, sb)
}

return sb.toString().trimEnd('\n', '#', ' ')
}

private fun splitDoc(doc: String): List<RefDoc> {
val refDocs = mutableListOf<RefDoc>()
val lines = doc.lines()
var ref: String? = null
val sb = StringBuilder()
for (line in lines) {
if (line.startsWith(REF)) {
if (ref != null) {
refDocs.add(RefDoc(ref, sb.toString()))
sb.clear()
}
ref = line.substring(REF.length)
} else {
sb.append(line).append("\n")
}
}
if (ref != null) {
refDocs.add(RefDoc(ref, sb.toString()))
}
return refDocs
}

private fun parseRequest(
host: String,
request: Request,
sb: StringBuilder
) {
sb.appendRef(request)
val apiName = request.name ?: (request.method + ":" + request.path?.url())
sb.append("### $apiName\n\n")
if (!request.desc.isNullOrEmpty()) {
request.desc!!.lines().forEach {
sb.append("// ").append(it).append("\n")
}
}

sb.append(request.method).append(" ")
.append(RequestUtils.concatPath(host, request.path?.url() ?: ""))
if (!request.querys.isNullOrEmpty()) {
val query = request.querys!!.joinToString("&") { "${it.name}=${it.value ?: ""}" }
if (query.isNotEmpty()) {
sb.append("?").append(query)
}
}

sb.append("\n")

request.headers?.forEach { header ->
sb.appendHeader(header.name ?: "", header.value)
}

val contentType = request.getContentType()
when {
contentType?.contains("application/json") == true -> {
request.body?.let { body ->
sb.append("\n")
sb.append(RequestUtils.parseRawBody(body))
}
}

contentType?.contains("application/x-www-form-urlencoded") == true -> {
request.formParams?.let { formParams ->
val formData = formParams.joinToString("&") { "${it.name}=${it.value ?: ""}" }
sb.append("\n")
sb.append(formData)
}
}

contentType?.contains("multipart/form-data") == true -> {
request.formParams?.let { formParams ->
sb.append("\n")
sb.append("Content-Type: multipart/form-data; boundary=WebAppBoundary\n")
for (param in formParams) {
sb.append("\n--WebAppBoundary\n")
if (param.type == "file") {
sb.append("Content-Disposition: form-data; name=\"${param.name}\"; filename=\"${param.value ?: "file"}\"\n")
sb.append("\n< ./relative/path/to/${param.value ?: "file"}\n")
} else {
sb.append("Content-Disposition: form-data; name=\"${param.name}\"\n")
sb.append("\n${param.value ?: "[${param.name}]"}\n")
}
sb.append("--WebAppBoundary--\n")
}
}
}
}

sb.appendEnd()
}

private fun StringBuilder.appendEnd() {
append("\n\n###\n\n")
}

private fun StringBuilder.appendHeader(name: String, value: String?) =
append(name).append(": ").append(value ?: "").append("\n")

private fun StringBuilder.appendRef(request: Request) =
append(REF).append(request.ref())
.append("\n")

private fun Request.ref(): String = resource()?.let {
actionContext.callInReadUI { PsiClassUtils.fullNameOfMember(it) }
} ?: IDUtils.shortUUID()
}

typealias RefDoc = Pair<String, String>
Loading

0 comments on commit 77e6e10

Please sign in to comment.