Skip to content

feat: autogenerate bindings in build script #6

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 14 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .gitattributes
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
hydrozoa_api.json linguist-generated=true
28 changes: 27 additions & 1 deletion .idea/gradle.xml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion .idea/kotlinc.xml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion .idea/misc.xml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

35 changes: 35 additions & 0 deletions bindings-generator/build.gradle.kts
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
/*
* This file was generated by the Gradle 'init' task.
*
* This generated file contains a sample Gradle plugin project to get you started.
* For more details on writing Custom Plugins, please refer to https://docs.gradle.org/8.10/userguide/custom_plugins.html in the Gradle documentation.
* This project uses @Incubating APIs which are subject to change.
*/

plugins {
// Apply the Java Gradle plugin development plugin to add support for developing Gradle plugins
`java-gradle-plugin`

// // Apply the Kotlin JVM plugin to add support for Kotlin.
kotlin("jvm") version libs.versions.kotlin
kotlin("plugin.serialization") version libs.versions.kotlin
}

repositories {
// Use Maven Central for resolving dependencies.
mavenCentral()
}

dependencies {
implementation("com.github.javaparser:javaparser-core:3.26.4")
implementation("org.jetbrains.kotlinx:kotlinx-serialization-json:1.8.1")
implementation("com.google.guava:guava:33.4.8-jre")
}

gradlePlugin {
// Define the plugin
val bindingsGen by plugins.creating {
id = "dev.vexide.hydrozoa.plugin.bindings"
implementationClass = "dev.vexide.hydrozoa.plugin.bindings.BindingsGeneratorPlugin"
}
}
7 changes: 7 additions & 0 deletions bindings-generator/settings.gradle.kts
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
dependencyResolutionManagement {
versionCatalogs {
create("libs") {
from(files("../gradle/libs.versions.toml"))
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
/*
* This source file was generated by the Gradle 'init' task
*/
package dev.vexide.hydrozoa.plugin.bindings

import dev.vexide.hydrozoa.plugin.bindings.tasks.GenerateBindingsTask
import org.gradle.api.Project
import org.gradle.api.Plugin

@Suppress("unused")
class BindingsGeneratorPlugin : Plugin<Project> {
override fun apply(project: Project) {
project.tasks.register("generateBindings", GenerateBindingsTask::class.java) { task ->
task.group = "build"
task.description = "Generates bindings for the Hydrozoa SDK"
}
}
}

Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
package dev.vexide.hydrozoa.plugin.bindings

import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable

@Serializable
data class SdkModule(
val name: String,
val items: List<SdkItem>,
val enums: List<SdkEnum>,
)

@Serializable
data class SdkItem(
val name: String,
val params: List<Param>,
val returns: Type?,
) {
@Serializable
data class Param(
val name: String,
val type: Type,
)

@Serializable
sealed class Type {
@Serializable
@SerialName("Bool")
object Bool : Type()

@Serializable
@SerialName("Int")
object Int : Type()

@Serializable
@SerialName("Long")
object Long : Type()

@Serializable
@SerialName("Float")
object Float : Type()

@Serializable
@SerialName("Double")
object Double : Type()

@Serializable
@SerialName("StringPtr")
object StringPtr : Type()

@Serializable
@SerialName("Named")
data class Named(val name: String) : Type()

@Serializable
@SerialName("Pointer")
data class Pointer(val destination: Type) : Type()
}
}

@Serializable
data class SdkEnum(
val name: String,
@SerialName("underlying_type")
val underlyingType: SdkItem.Type,
val variants: Map<String, Double>,
)
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
package dev.vexide.hydrozoa.plugin.bindings

import com.github.javaparser.ast.CompilationUnit
import com.github.javaparser.ast.Modifier
import com.github.javaparser.ast.Modifier.Keyword
import com.github.javaparser.ast.Node
import com.github.javaparser.ast.NodeList
import com.github.javaparser.ast.body.ClassOrInterfaceDeclaration
import com.github.javaparser.ast.expr.StringLiteralExpr
import com.github.javaparser.ast.nodeTypes.NodeWithAnnotations

fun CompilationUnit.addHydrozoaGeneratedComment(): CompilationUnit {
this.setBlockComment(" This file was automatically @generated by the Hydrozoa bindings generator. Do not edit this manually!\n"
+ " Instead, update the Hydrozoa SDK at <https://github.com/vexide/hydrozoa> and re-run the generator. ")
return this
}

fun<N: Node> NodeWithAnnotations<N>.addHydrozoaGeneratedAnnotation(): N {
return this.addSingleMemberAnnotation(
javax.annotation.processing.Generated::class.java,
StringLiteralExpr("dev.vexide.hydrozoa.plugin.bindings.BindingsGeneratorPlugin")
)
}

fun ClassOrInterfaceDeclaration.addStaticInitAnnotation(): ClassOrInterfaceDeclaration {
return this.addMarkerAnnotation("org.teavm.interop.StaticInit")
}

fun<T: NodeWithAnnotations<N>, N: Node> T.addNotNullAnnotation(add: Boolean = true): T {
if (add) {
addMarkerAnnotation("org.jetbrains.annotations.NotNull")
}
return this
}

fun ClassOrInterfaceDeclaration.addPrivateConstructor(): ClassOrInterfaceDeclaration {
this.addConstructor(Keyword.PRIVATE)
return this
}

fun<T: Node> nodeListOf(vararg items: T): NodeList<T> {
return NodeList.nodeList(*items)
}

fun modifierListOf(vararg modifiers: Keyword): NodeList<Modifier> {
return Modifier.createModifierList(*modifiers)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,129 @@
package dev.vexide.hydrozoa.plugin.bindings.sdk

import com.github.javaparser.ast.Modifier
import com.github.javaparser.ast.body.RecordDeclaration
import com.github.javaparser.ast.expr.CastExpr
import com.github.javaparser.ast.expr.DoubleLiteralExpr
import com.github.javaparser.ast.expr.Expression
import com.github.javaparser.ast.expr.IntegerLiteralExpr
import com.github.javaparser.ast.expr.NameExpr
import com.github.javaparser.ast.expr.ObjectCreationExpr
import com.github.javaparser.ast.stmt.BlockStmt
import com.github.javaparser.ast.stmt.ReturnStmt
import com.github.javaparser.ast.type.ClassOrInterfaceType
import com.github.javaparser.utils.SourceRoot
import com.google.common.base.CaseFormat
import dev.vexide.hydrozoa.plugin.bindings.SdkEnum
import dev.vexide.hydrozoa.plugin.bindings.addHydrozoaGeneratedAnnotation
import dev.vexide.hydrozoa.plugin.bindings.addHydrozoaGeneratedComment
import dev.vexide.hydrozoa.plugin.bindings.modifierListOf
import dev.vexide.hydrozoa.plugin.bindings.nodeListOf
import kotlin.collections.iterator

class JavaSdkEnum(val sdk: SdkEnum, val module: JavaSdkModule) {
fun generate(sourceRoot: SourceRoot) {
val name = generateEnumName(sdk.name, module.sdk.name)

val cu = makeCompilationUnit(name, sourceRoot)
.addHydrozoaGeneratedComment()

val underlyingType = module.javaTypeFor(sdk.underlyingType)
val record = RecordDeclaration(
modifierListOf(Modifier.Keyword.PUBLIC),
name,
)
.apply { cu.addType(this) }
.addHydrozoaGeneratedAnnotation()
.setJavadocComment(sdk.name)
.addParameter(underlyingType, "value")

val recordType = ClassOrInterfaceType(null, record.name, null)

for ((variantName, variantValue) in sdk.variants) {
val literal: Expression = if (variantValue % 1.0 == 0.0) {
IntegerLiteralExpr(variantValue.toInt().toString())
} else {
DoubleLiteralExpr(variantValue)
}

record.addFieldWithInitializer(
underlyingType,
variantName,
CastExpr(underlyingType, literal),
Modifier.Keyword.PUBLIC,
Modifier.Keyword.STATIC,
Modifier.Keyword.FINAL,
)

record
.addFieldWithInitializer(
recordType,
generateMemberName(variantName, name),
ObjectCreationExpr(null, recordType, nodeListOf(NameExpr(variantName))),
Modifier.Keyword.PUBLIC,
Modifier.Keyword.STATIC,
Modifier.Keyword.FINAL,
)
.setJavadocComment(variantName)
}

record.addMethod("getRawValue", Modifier.Keyword.PUBLIC)
.setType(underlyingType)
.setBody(
BlockStmt(
nodeListOf(ReturnStmt(NameExpr("value")))
)
)
}

companion object {
fun generateEnumName(name: String, moduleName: String): String {
val name = name
.removePrefix("V5_")
.removePrefix("V5")

val components = ArrayDeque(
CaseFormat.LOWER_CAMEL
.to(CaseFormat.LOWER_UNDERSCORE, name)
.split('_')
)

components.addFirst(moduleName)

return CaseFormat.LOWER_UNDERSCORE
.to(CaseFormat.UPPER_CAMEL, components.joinToString("_"))
}

fun generateMemberName(name: String, enumName: String): String {
val components = ArrayDeque(
CaseFormat.LOWER_CAMEL
.to(CaseFormat.LOWER_UNDERSCORE, name)
.split('_'))
val enumComponents = ArrayDeque(
CaseFormat.LOWER_CAMEL
.to(CaseFormat.LOWER_UNDERSCORE, enumName)
.split('_'))

// Remove module prefix from enum name
enumComponents.removeFirst()

for (redundantComponent in arrayOf("k", "v5")) {
if (components.firstOrNull() == redundantComponent) {
components.removeFirst()
} else {
break
}
}

for (typeNameComponent in enumComponents) {
if (components.firstOrNull() == typeNameComponent) {
components.removeFirst()
} else {
break
}
}

return components.joinToString("_").uppercase()
}
}
}
Loading