-
Notifications
You must be signed in to change notification settings - Fork 146
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat: Add support for exporting APIs as .http files
- Loading branch information
Showing
11 changed files
with
2,172 additions
and
17 deletions.
There are no files selected for viewing
81 changes: 81 additions & 0 deletions
81
idea-plugin/src/main/kotlin/com/itangcent/idea/plugin/api/export/http/HttpClientExporter.kt
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,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 | ||
) | ||
} | ||
} | ||
} | ||
} | ||
} | ||
} |
63 changes: 63 additions & 0 deletions
63
idea-plugin/src/main/kotlin/com/itangcent/idea/plugin/api/export/http/HttpClientFileSaver.kt
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,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) | ||
} | ||
} | ||
} |
176 changes: 176 additions & 0 deletions
176
idea-plugin/src/main/kotlin/com/itangcent/idea/plugin/api/export/http/HttpClientFormatter.kt
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,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> |
Oops, something went wrong.