Skip to content

Commit

Permalink
Support generate http transcoding requests
Browse files Browse the repository at this point in the history
  • Loading branch information
devkanro committed Dec 19, 2023
1 parent ce15e02 commit 5b49d46
Show file tree
Hide file tree
Showing 23 changed files with 553 additions and 35 deletions.
2 changes: 1 addition & 1 deletion gradle.properties
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
pluginGroup=io.kanro.idea.plugin.protobuf
pluginName=IntelliJ Protobuf Language Plugin
# SemVer format -> https://semver.org
pluginVersion=1.7.30
pluginVersion=1.7.40
# See https://plugins.jetbrains.com/docs/intellij/build-number-ranges.html
# for insight into build numbers and IntelliJ Platform versions.
pluginSinceBuild=233
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ object ProtobufIcons {
val IMPLEMENTING_RPC: Icon = loadIcon("implementingRpc.svg")

val PROCEDURE: Icon = loadIcon("procedure.svg")
val PROCEDURE_HTTP: Icon = loadIcon("procedureHttp.svg")
val PROTO_DECOMPILE: Icon = loadIcon("proto_decompile.svg")

val ARRANGE_FROM_MIN: Icon = loadIcon("arrangeFromMin.svg")
Expand Down
17 changes: 17 additions & 0 deletions src/main/kotlin/io/kanro/idea/plugin/protobuf/aip/AipOptions.kt
Original file line number Diff line number Diff line change
Expand Up @@ -11,8 +11,25 @@ object AipOptions {
val resourceChildTypeField = QualifiedName.fromComponents("child_type")

val httpRuleBodyName = QualifiedName.fromComponents("google", "api", "HttpRule", "body")
val httpRuleBodyField = QualifiedName.fromComponents("body")
val httpRuleResponseBodyName = QualifiedName.fromComponents("google", "api", "HttpRule", "response_body")

val lroMetadataName = QualifiedName.fromComponents("google", "longrunning", "OperationInfo", "metadata_type")
val lroResponseName = QualifiedName.fromComponents("google", "longrunning", "OperationInfo", "response_type")

val httpOption = QualifiedName.fromComponents("google", "api", "http")
val httpRuleGetName = QualifiedName.fromComponents("google", "api", "HttpRule", "get")
val httpRulePutName = QualifiedName.fromComponents("google", "api", "HttpRule", "put")
val httpRulePostName = QualifiedName.fromComponents("google", "api", "HttpRule", "post")
val httpRuleDeleteName = QualifiedName.fromComponents("google", "api", "HttpRule", "delete")
val httpRulePatchName = QualifiedName.fromComponents("google", "api", "HttpRule", "patch")

val httpRulesName =
setOf(
httpRuleGetName,
httpRulePutName,
httpRulePostName,
httpRuleDeleteName,
httpRulePatchName,
)
}
25 changes: 24 additions & 1 deletion src/main/kotlin/io/kanro/idea/plugin/protobuf/aip/Extension.kt
Original file line number Diff line number Diff line change
@@ -1,6 +1,29 @@
package io.kanro.idea.plugin.protobuf.aip

import com.intellij.psi.util.CachedValueProvider
import com.intellij.psi.util.CachedValuesManager
import com.intellij.psi.util.PsiModificationTracker
import io.kanro.idea.plugin.protobuf.lang.psi.ProtobufMessageDefinition
import io.kanro.idea.plugin.protobuf.lang.psi.ProtobufRpcDefinition
import io.kanro.idea.plugin.protobuf.lang.psi.nullCachedValue
import io.kanro.idea.plugin.protobuf.lang.psi.stringValue

fun ProtobufRpcDefinition.aipMethod() {
internal fun ProtobufRpcDefinition.transcodingBody(): String? {
return CachedValuesManager.getCachedValue(this) {
val option =
options(AipOptions.httpOption).lastOrNull()
?: return@getCachedValue nullCachedValue()

val result = option.value(AipOptions.httpRuleBodyField)?.stringValue() ?: ""
CachedValueProvider.Result.create(result, PsiModificationTracker.MODIFICATION_COUNT)
}
}

internal fun ProtobufRpcDefinition.resolveInput(): ProtobufMessageDefinition? {
return CachedValuesManager.getCachedValue(this) {
val input =
this.rpcIOList.firstOrNull()?.typeName?.reference?.resolve() as? ProtobufMessageDefinition
?: return@getCachedValue nullCachedValue()
return@getCachedValue CachedValueProvider.Result.create(input, PsiModificationTracker.MODIFICATION_COUNT)
}
}
26 changes: 23 additions & 3 deletions src/main/kotlin/io/kanro/idea/plugin/protobuf/grpc/Utils.kt
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,16 @@ fun JsonElement.injectedRequest(): HttpRequest? {
return host.parentOfType()
}

fun HttpRequest.isGrpcRequest(): Boolean {
fun HttpRequest.isTranscoding(): Boolean {
return CachedValuesManager.getCachedValue(this) {
CachedValueProvider.Result.create(
method?.text in GrpcRequestExecutionSupport.supportedTranscodingMethod && getHeaderField("grpc-method") != null,
PsiModificationTracker.MODIFICATION_COUNT,
)
}
}

fun HttpRequest.isNativeGrpc(): Boolean {
return CachedValuesManager.getCachedValue(this) {
CachedValueProvider.Result.create(
method?.text in GrpcRequestExecutionSupport.supportedMethod,
Expand All @@ -29,11 +38,22 @@ fun HttpRequest.isGrpcRequest(): Boolean {
}
}

fun HttpRequest.grpcMethod(): ProtobufRpcDefinition? {
fun HttpRequest.isGrpcRequest(): Boolean {
return isNativeGrpc() || isTranscoding()
}

fun HttpRequest.resolveRpc(): ProtobufRpcDefinition? {
if (!isGrpcRequest()) return null
return CachedValuesManager.getCachedValue(this) {
val path =
if (method?.text in GrpcRequestExecutionSupport.supportedMethod) {
requestTarget?.pathAbsolute?.text?.trim('/')
} else {
getHeaderField("grpc-method")?.headerFieldValue?.text?.trim('/', ' ')
}

val result =
requestTarget?.pathAbsolute?.text?.trim('/')?.let {
path?.let {
StubIndex.getElements(
ServiceMethodIndex.key,
it,
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
package io.kanro.idea.plugin.protobuf.grpc.gutter

import com.google.api.pathtemplate.PathTemplate
import com.intellij.codeInsight.daemon.RelatedItemLineMarkerInfo
import com.intellij.codeInsight.daemon.RelatedItemLineMarkerProvider
import com.intellij.codeInsight.template.impl.TemplateImpl
import com.intellij.httpClient.actions.generation.HttpRequestUrlPathInfo
import com.intellij.httpClient.actions.generation.HttpRequestUrlsGenerationRequest
import com.intellij.httpClient.actions.generation.RequestUrlContextInfo
import com.intellij.httpClient.executor.util.unwrap
import com.intellij.httpClient.http.request.microservices.OpenInHttpClientLineMarkerBuilder
import com.intellij.httpClient.http.request.microservices.RequestContextData
import com.intellij.psi.PsiElement
import com.intellij.psi.util.parentOfType
import io.kanro.idea.plugin.protobuf.ProtobufIcons
import io.kanro.idea.plugin.protobuf.aip.AipOptions
import io.kanro.idea.plugin.protobuf.lang.psi.ProtobufFieldAssign
import io.kanro.idea.plugin.protobuf.lang.psi.ProtobufRpcOption
import io.kanro.idea.plugin.protobuf.lang.psi.firstLeaf
import io.kanro.idea.plugin.protobuf.lang.psi.primitive.element.ProtobufRpcDefinition
import io.kanro.idea.plugin.protobuf.lang.psi.stringValue
import javax.swing.Icon

class AipRunRequestGutterProvider : RelatedItemLineMarkerProvider() {
override fun getIcon(): Icon {
return ProtobufIcons.PROCEDURE
}

override fun getId(): String {
return "AipRunRequestGutterProvider"
}

override fun getName(): String {
return "Run gRpc via HTTP transcoding"
}

@Suppress("UnstableApiUsage")
override fun collectNavigationMarkers(
element: PsiElement,
result: MutableCollection<in RelatedItemLineMarkerInfo<*>>,
) {
if (element !is ProtobufFieldAssign) return
val option = element.parentOfType<ProtobufRpcOption>() ?: return
val grpcDefinition = element.parentOfType<ProtobufRpcDefinition>() ?: return
if (!option.isOption(AipOptions.httpOption)) return
val fieldName = element.field()?.qualifiedName() ?: return
if (fieldName !in AipOptions.httpRulesName) return
val httpMethod = fieldName.lastComponent?.uppercase() ?: return
val path = element.constant?.stringValue() ?: return

val service = grpcDefinition.owner() ?: return
val serviceName = service.qualifiedName() ?: return
val methodName = grpcDefinition.name() ?: return
val hasBody = httpMethod in listOf("POST", "PUT", "PATCH") && option.value(AipOptions.httpRuleBodyField) != null

val request =
HttpRequestUrlsGenerationRequest(
listOfNotNull(
HttpRequestUrlPathInfo.create(
element.project,
PathTemplate.create(path).withoutVars().toString(),
listOf(httpMethod),
).unwrap(false),
),
RequestUrlContextInfo.create(
element.project,
listOf("http://", "https://"),
listOf("localhost:8080"),
buildCustomRequestBodyTemplate(serviceName.toString(), methodName, hasBody),
).unwrap(false) ?: return,
)

result +=
OpenInHttpClientLineMarkerBuilder.fromGenerationRequest(element.project, request)
.createLineMarkerInfo(element.firstLeaf(), ProtobufIcons.PROCEDURE_HTTP)
}

private fun buildCustomRequestBodyTemplate(
serviceName: String,
methodName: String,
hasBody: Boolean,
): RequestContextData {
return if (hasBody) {
RequestContextData.CustomRequestBodyTemplate(
TemplateImpl(
"transcodingWithBody",
"\ngrpc-method: $serviceName/$methodName\ncontent-type: application/json\n\n{\n}",
"grpc",
),
)
} else {
RequestContextData.CustomRequestBodyTemplate(
TemplateImpl(
"transcodingWithoutBody",
"\ngrpc-method: $serviceName/$methodName",
"grpc",
),
)
}
}
}
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
package io.kanro.idea.plugin.protobuf.grpc.referece

import com.intellij.httpClient.http.request.psi.HttpQueryParameterKey
import com.intellij.httpClient.http.request.psi.HttpRequest
import com.intellij.json.psi.JsonObject
import com.intellij.json.psi.JsonProperty
import com.intellij.json.psi.JsonStringLiteral
Expand All @@ -9,15 +11,20 @@ import com.intellij.psi.util.CachedValuesManager
import com.intellij.psi.util.PsiModificationTracker
import com.intellij.psi.util.QualifiedName
import com.intellij.psi.util.parentOfType
import io.kanro.idea.plugin.protobuf.grpc.grpcMethod
import io.kanro.idea.plugin.protobuf.aip.AipOptions
import io.kanro.idea.plugin.protobuf.aip.resolveInput
import io.kanro.idea.plugin.protobuf.grpc.injectedRequest
import io.kanro.idea.plugin.protobuf.grpc.isTranscoding
import io.kanro.idea.plugin.protobuf.grpc.resolveRpc
import io.kanro.idea.plugin.protobuf.lang.psi.ProtobufMessageDefinition
import io.kanro.idea.plugin.protobuf.lang.psi.nullCachedValue
import io.kanro.idea.plugin.protobuf.lang.psi.resolveField
import io.kanro.idea.plugin.protobuf.lang.psi.resolveFieldType
import io.kanro.idea.plugin.protobuf.lang.psi.stringValue

internal fun JsonProperty.contextJsonObject(): JsonObject? {
return CachedValuesManager.getCachedValue(this) {
var obj = this.parent as? JsonObject ?: return@getCachedValue null
var obj = this.parent as? JsonObject ?: return@getCachedValue nullCachedValue()
while (true) {
obj.findProperty("@type")?.let {
return@getCachedValue CachedValueProvider.Result.create(obj, PsiModificationTracker.MODIFICATION_COUNT)
Expand All @@ -28,23 +35,49 @@ internal fun JsonProperty.contextJsonObject(): JsonObject? {
}
}

internal fun HttpRequest.grpcBodyType(): ProtobufMessageDefinition? {
return CachedValuesManager.getCachedValue(this) {
val rpcDefinition = resolveRpc() ?: return@getCachedValue nullCachedValue()
val input = rpcDefinition.resolveInput() ?: return@getCachedValue nullCachedValue()

val body =
if (isTranscoding()) {
val option =
rpcDefinition.options(AipOptions.httpOption).lastOrNull()
?: return@getCachedValue nullCachedValue()
val body =
option.value(AipOptions.httpRuleBodyField)?.stringValue() ?: return@getCachedValue nullCachedValue()
if (body != "*") {
input.resolveFieldType(QualifiedName.fromDottedString(body), true) as? ProtobufMessageDefinition
} else {
input
}
} else {
input
}

return@getCachedValue CachedValueProvider.Result.create(body, PsiModificationTracker.MODIFICATION_COUNT)
}
}

internal fun JsonProperty.contextMessage(): ProtobufMessageDefinition? {
return CachedValuesManager.getCachedValue(this) {
val contextJsonObject = contextJsonObject()
if (contextJsonObject == null) {
val request = injectedRequest() ?: return@getCachedValue null
val rpcDefinition = request.grpcMethod() ?: return@getCachedValue null
val input =
rpcDefinition.rpcIOList.firstOrNull()?.typeName?.reference?.resolve() as? ProtobufMessageDefinition
?: return@getCachedValue null
return@getCachedValue CachedValueProvider.Result.create(input, PsiModificationTracker.MODIFICATION_COUNT)
val request = injectedRequest() ?: return@getCachedValue nullCachedValue()
val rpcDefinition = request.grpcBodyType() ?: return@getCachedValue nullCachedValue()

return@getCachedValue CachedValueProvider.Result.create(
rpcDefinition,
PsiModificationTracker.MODIFICATION_COUNT,
)
}
val typeUrl =
contextJsonObject.findProperty("@type")?.value as? JsonStringLiteral
?: return@getCachedValue null
?: return@getCachedValue nullCachedValue()
val input =
typeUrl.references.firstOrNull { it is GrpcTypeUrlReference }?.resolve() as? ProtobufMessageDefinition
?: return@getCachedValue null
?: return@getCachedValue nullCachedValue()
return@getCachedValue CachedValueProvider.Result.create(input, PsiModificationTracker.MODIFICATION_COUNT)
}
}
Expand All @@ -70,8 +103,8 @@ internal fun JsonProperty.qualifiedName(): QualifiedName? {

internal fun JsonProperty.resolveParentType(): PsiElement? {
return CachedValuesManager.getCachedValue(this) {
val contextMessage = contextMessage() ?: return@getCachedValue null
val qualifiedName = qualifiedName() ?: return@getCachedValue null
val contextMessage = contextMessage() ?: return@getCachedValue nullCachedValue()
val qualifiedName = qualifiedName() ?: return@getCachedValue nullCachedValue()
CachedValueProvider.Result.create(
contextMessage.resolveFieldType(qualifiedName.removeTail(1), true),
PsiModificationTracker.MODIFICATION_COUNT,
Expand All @@ -81,11 +114,23 @@ internal fun JsonProperty.resolveParentType(): PsiElement? {

internal fun JsonProperty.resolve(): PsiElement? {
return CachedValuesManager.getCachedValue(this) {
val contextMessage = contextMessage() ?: return@getCachedValue null
val qualifiedName = qualifiedName() ?: return@getCachedValue null
val contextMessage = contextMessage() ?: return@getCachedValue nullCachedValue()
val qualifiedName = qualifiedName() ?: return@getCachedValue nullCachedValue()
CachedValueProvider.Result.create(
contextMessage.resolveField(qualifiedName, true),
PsiModificationTracker.MODIFICATION_COUNT,
)
}
}

internal fun HttpQueryParameterKey.resolve(): PsiElement? {
return CachedValuesManager.getCachedValue(this) {
val request = parentOfType<HttpRequest>() ?: return@getCachedValue nullCachedValue()
val rpc = request.resolveRpc() ?: return@getCachedValue nullCachedValue()
val input = rpc.resolveInput() ?: return@getCachedValue nullCachedValue()
val field =
input.resolveField(QualifiedName.fromDottedString(this.text), true)
?: return@getCachedValue nullCachedValue()
CachedValueProvider.Result.create(field, PsiModificationTracker.MODIFICATION_COUNT)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ import com.intellij.psi.PsiReferenceContributor
import com.intellij.psi.PsiReferenceRegistrar
import com.intellij.psi.util.parentOfType
import com.intellij.util.ProcessingContext
import io.kanro.idea.plugin.protobuf.grpc.request.GrpcRequestExecutionSupport
import io.kanro.idea.plugin.protobuf.grpc.isGrpcRequest

object GrpcJsonBody : PatternCondition<JsonElement>("GRPC JSON BODY") {
override fun accepts(
Expand All @@ -23,7 +23,7 @@ object GrpcJsonBody : PatternCondition<JsonElement>("GRPC JSON BODY") {
InjectedLanguageManager.getInstance(t.project).getInjectionHost(t) as? HttpMessageBody
?: return false
val request = host.parentOfType<HttpRequest>() ?: return false
return request.method?.text in GrpcRequestExecutionSupport.supportedMethod
return request.isGrpcRequest()
}
}

Expand Down
Loading

0 comments on commit 5b49d46

Please sign in to comment.