-
Notifications
You must be signed in to change notification settings - Fork 22
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request #256 from LossyDragon/generateUnifiedMethods
Automatically generate unified methods
- Loading branch information
Showing
61 changed files
with
580 additions
and
2,442 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
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
10 changes: 10 additions & 0 deletions
10
buildSrc/src/main/kotlin/in/dragonbra/generators/rpc/RpcGenPlugin.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,10 @@ | ||
package `in`.dragonbra.generators.rpc | ||
|
||
import org.gradle.api.Plugin | ||
import org.gradle.api.Project | ||
|
||
class RpcGenPlugin : Plugin<Project> { | ||
override fun apply(project: Project) { | ||
project.tasks.register("generateRpcMethods", RpcGenTask::class.java) | ||
} | ||
} |
40 changes: 40 additions & 0 deletions
40
buildSrc/src/main/kotlin/in/dragonbra/generators/rpc/RpcGenTask.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,40 @@ | ||
package `in`.dragonbra.generators.rpc | ||
|
||
import `in`.dragonbra.generators.rpc.parser.ProtoParser | ||
import org.gradle.api.DefaultTask | ||
import org.gradle.api.tasks.TaskAction | ||
import java.io.File | ||
|
||
open class RpcGenTask : DefaultTask() { | ||
|
||
companion object { | ||
private const val KDOC_AUTHOR = "Lossy" | ||
private const val KDOC_DATE = "2024-04-10" | ||
|
||
val kDocClass = """ | ||
|@author $KDOC_AUTHOR | ||
|@since $KDOC_DATE | ||
""" | ||
.trimMargin() | ||
} | ||
|
||
private val outputDir = File( | ||
project.layout.buildDirectory.get().asFile, | ||
"generated/source/javasteam/main/java/" | ||
) | ||
|
||
private val protoDirectory = project.file("src/main/proto") | ||
|
||
@TaskAction | ||
fun generate() { | ||
println("Generating RPC service methods as interfaces") | ||
|
||
outputDir.mkdirs() | ||
|
||
val protoParser = ProtoParser(outputDir) | ||
|
||
protoDirectory.walkTopDown() | ||
.filter { it.isFile && it.extension == "proto" } | ||
.forEach(protoParser::parseFile) | ||
} | ||
} |
222 changes: 222 additions & 0 deletions
222
buildSrc/src/main/kotlin/in/dragonbra/generators/rpc/parser/ProtoParser.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,222 @@ | ||
package `in`.dragonbra.generators.rpc.parser | ||
|
||
import com.squareup.kotlinpoet.* | ||
import com.squareup.kotlinpoet.ParameterizedTypeName.Companion.parameterizedBy | ||
import `in`.dragonbra.generators.rpc.RpcGenTask | ||
import java.io.File | ||
import java.util.* | ||
|
||
class ProtoParser(private val outputDir: File) { | ||
|
||
private companion object { | ||
private const val RPC_PACKAGE = "in.dragonbra.javasteam.rpc" | ||
private const val SERVICE_PACKAGE = "${RPC_PACKAGE}.service" | ||
private const val INTERFACE_PACKAGE = "$RPC_PACKAGE.interfaces" | ||
|
||
private val suppressAnnotation = AnnotationSpec | ||
.builder(Suppress::class) | ||
.addMember("%S", "KDocUnresolvedReference") // IntelliJ's seems to get confused with canonical names | ||
.addMember("%S", "RedundantVisibilityModifier") // KotlinPoet is an explicit API generator | ||
.addMember("%S", "unused") // All methods could be used. | ||
.build() | ||
|
||
private val classAsyncJobSingle = ClassName( | ||
"in.dragonbra.javasteam.types", | ||
"AsyncJobSingle" | ||
) | ||
private val classServiceMethodResponse = ClassName( | ||
"in.dragonbra.javasteam.steam.handlers.steamunifiedmessages.callback", | ||
"ServiceMethodResponse" | ||
) | ||
|
||
private val kDocNoResponse = """|No return value.""".trimMargin() | ||
private fun kDocReturns(requestClassName: ClassName, returnClassName: ClassName): String = """ | ||
|@param request The request. | ||
|@see [${requestClassName.simpleName}] | ||
|@returns [${returnClassName.canonicalName}] | ||
""".trimMargin() | ||
} | ||
|
||
/** | ||
* Open a .proto file and find all service interfaces. | ||
* Then grab the name of the RPC interface name and everything between the curly braces | ||
* Then loop through all RPC interface methods, destructuring them to name, type, and response and put them in a list. | ||
* Collect the items into a [Service] and pass it off to [buildInterface] and [buildClass] | ||
*/ | ||
fun parseFile(file: File) { | ||
Regex("""service\s+(\w+)\s*\{([^}]*)}""") | ||
.findAll(file.readText()) | ||
.forEach { serviceMatch -> | ||
val serviceMethods = mutableListOf<ServiceMethod>() // Method list | ||
|
||
val serviceName = serviceMatch.groupValues[1] | ||
val methodsContent = serviceMatch.groupValues[2] | ||
|
||
Regex("""rpc\s+(\w+)\s*\((.*?)\)\s*returns\s*\((.*?)\);""") | ||
.findAll(methodsContent) | ||
.forEach { methodMatch -> | ||
val (methodName, requestType, responseType) = methodMatch.destructured | ||
val request = requestType.trim().replace(".", "") | ||
val response = responseType.trim().replace(".", "") | ||
|
||
ServiceMethod(methodName, request, response).also(serviceMethods::add) | ||
} | ||
|
||
Service(serviceName, serviceMethods).also { service -> | ||
println("[${file.name}] - found \"${service.name}\", which has ${service.methods.size} methods") | ||
|
||
buildInterface(file, service) | ||
buildClass(file, service) | ||
} | ||
} | ||
} | ||
|
||
/** | ||
* Transforms the .proto file into an import statement. | ||
* Also handle edge cases if they are discovered. | ||
* | ||
* Example: steammessages_contentsystem.steamclient.proto to SteammessagesContentsystemSteamclient | ||
*/ | ||
private fun transformProtoFileName(protoFileName: String): String { | ||
// Edge cases | ||
if (protoFileName == "steammessages_remoteclient_service.steamclient.proto") { | ||
return "SteammessagesRemoteclientServiceMessages" | ||
} | ||
|
||
return protoFileName | ||
.removeSuffix(".proto") | ||
.split("[._]".toRegex()) | ||
.joinToString("") { str -> | ||
str.replaceFirstChar { char -> | ||
if (char.isLowerCase()) { | ||
char.titlecase(Locale.getDefault()) | ||
} else { | ||
char.toString() | ||
} | ||
} | ||
} | ||
} | ||
|
||
/** | ||
* Build the [Service] to an interface with all known RPC methods. | ||
*/ | ||
private fun buildInterface(file: File, service: Service) { | ||
// Interface Builder | ||
val iBuilder = TypeSpec | ||
.interfaceBuilder("I${service.name}") | ||
.addAnnotation(suppressAnnotation) | ||
.addKdoc(RpcGenTask.kDocClass) | ||
|
||
// Iterate over found 'rpc' methods | ||
val protoFileName = transformProtoFileName(file.name) | ||
service.methods.forEach { method -> | ||
val requestClassName = ClassName( | ||
packageName = "in.dragonbra.javasteam.protobufs.steamclient.$protoFileName", | ||
method.requestType | ||
) | ||
|
||
// Make a method | ||
val funBuilder = FunSpec | ||
.builder(method.methodName.replaceFirstChar { it.lowercase(Locale.getDefault()) }) | ||
.addModifiers(KModifier.ABSTRACT) | ||
.addParameter("request", requestClassName) | ||
|
||
// Add method kDoc | ||
// Add `AsyncJobSingle<ServiceMethodResponse>` if there is a response | ||
if (method.responseType == "NoResponse") { | ||
funBuilder.addKdoc(kDocNoResponse) | ||
} else { | ||
val returnClassName = ClassName( | ||
packageName = "in.dragonbra.javasteam.protobufs.steamclient.$protoFileName", | ||
method.responseType | ||
) | ||
val kDoc = kDocReturns(requestClassName, returnClassName) | ||
funBuilder.addKdoc(kDoc) | ||
.returns(classAsyncJobSingle.parameterizedBy(classServiceMethodResponse)) | ||
} | ||
|
||
// Add the function to the interface class. | ||
iBuilder.addFunction(funBuilder.build()) | ||
} | ||
|
||
// Build everything together and write it | ||
FileSpec.builder(INTERFACE_PACKAGE, "I${service.name}") | ||
.addType(iBuilder.build()) | ||
.build() | ||
.writeTo(outputDir) | ||
} | ||
|
||
/** | ||
* Build the [Service] to a class with all known RPC methods. | ||
*/ | ||
private fun buildClass(file: File, service: Service) { | ||
// Class Builder | ||
val cBuilder = TypeSpec | ||
.classBuilder(service.name) | ||
.addAnnotation(suppressAnnotation) | ||
.addKdoc(RpcGenTask.kDocClass) | ||
.primaryConstructor( | ||
FunSpec.constructorBuilder() | ||
.addParameter( | ||
name = "steamUnifiedMessages", | ||
type = ClassName( | ||
packageName = "in.dragonbra.javasteam.steam.handlers.steamunifiedmessages", | ||
"SteamUnifiedMessages" | ||
) | ||
) | ||
.build() | ||
) | ||
.addSuperclassConstructorParameter("steamUnifiedMessages") | ||
.superclass( | ||
ClassName( | ||
packageName = "in.dragonbra.javasteam.steam.handlers.steamunifiedmessages", | ||
"UnifiedService" | ||
) | ||
) | ||
.addSuperinterface( | ||
ClassName( | ||
packageName = "in.dragonbra.javasteam.rpc.interfaces", | ||
"I${service.name}" | ||
) | ||
) | ||
|
||
// Iterate over found 'rpc' methods. | ||
val protoFileName = transformProtoFileName(file.name) | ||
service.methods.forEach { method -> | ||
val requestClassName = ClassName( | ||
packageName = "in.dragonbra.javasteam.protobufs.steamclient.$protoFileName", | ||
method.requestType | ||
) | ||
|
||
// Make a method | ||
val funBuilder = FunSpec | ||
.builder(method.methodName.replaceFirstChar { it.lowercase(Locale.getDefault()) }) | ||
.addModifiers(KModifier.OVERRIDE) | ||
.addParameter("request", requestClassName) | ||
|
||
// Add method kDoc | ||
// Add `AsyncJobSingle<ServiceMethodResponse>` if there is a response | ||
if (method.responseType == "NoResponse") { | ||
funBuilder.addKdoc(kDocNoResponse) | ||
.addStatement("sendNotification(request, %S)", method.methodName) | ||
} else { | ||
val returnClassName = ClassName( | ||
packageName = "in.dragonbra.javasteam.protobufs.steamclient.$protoFileName", | ||
method.responseType | ||
) | ||
val kDoc = kDocReturns(requestClassName, returnClassName) | ||
funBuilder.addKdoc(kDoc) | ||
.returns(classAsyncJobSingle.parameterizedBy(classServiceMethodResponse)) | ||
.addStatement("return sendMessage(request, %S)", method.methodName) | ||
} | ||
|
||
cBuilder.addFunction(funBuilder.build()) | ||
} | ||
|
||
// Build everything together and write it | ||
FileSpec.builder(SERVICE_PACKAGE, service.name) | ||
.addType(cBuilder.build()) | ||
.build() | ||
.writeTo(outputDir) | ||
} | ||
} |
3 changes: 3 additions & 0 deletions
3
buildSrc/src/main/kotlin/in/dragonbra/generators/rpc/parser/Service.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,3 @@ | ||
package `in`.dragonbra.generators.rpc.parser | ||
|
||
data class Service(val name: String, val methods: List<ServiceMethod>) |
3 changes: 3 additions & 0 deletions
3
buildSrc/src/main/kotlin/in/dragonbra/generators/rpc/parser/ServiceMethod.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,3 @@ | ||
package `in`.dragonbra.generators.rpc.parser | ||
|
||
data class ServiceMethod(val methodName: String, val requestType: String, val responseType: String) |
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
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 was deleted.
Oops, something went wrong.
Oops, something went wrong.