diff --git a/convention/src/main/kotlin/io/gitlab/jfronny/scripts/CodegenExt.kt b/convention/src/main/kotlin/io/gitlab/jfronny/scripts/CodegenExt.kt new file mode 100644 index 0000000..f441dc0 --- /dev/null +++ b/convention/src/main/kotlin/io/gitlab/jfronny/scripts/CodegenExt.kt @@ -0,0 +1,18 @@ +package io.gitlab.jfronny.scripts + +import io.gitlab.jfronny.scripts.codegen.ContentGenerator +import org.gradle.api.Action +import org.gradle.api.Project +import org.gradle.api.tasks.SourceSet +import org.gradle.kotlin.dsl.extra + +val Project.codegenDir get() = buildDir.resolve("generated/sources/jfCodegen") + +fun SourceSet.generate(project: Project, generate: Action) { + val generators = project.extra["codeGenerators"] as LinkedHashMap + val generator = ContentGenerator() + generate.execute(generator) + generators[name] = generator.finalize() + java.srcDir(project.codegenDir.resolve("java/$name")) + resources.srcDir(project.codegenDir.resolve("resources/$name")) +} \ No newline at end of file diff --git a/convention/src/main/kotlin/io/gitlab/jfronny/scripts/codegen/ClassGenerator.kt b/convention/src/main/kotlin/io/gitlab/jfronny/scripts/codegen/ClassGenerator.kt new file mode 100644 index 0000000..d325839 --- /dev/null +++ b/convention/src/main/kotlin/io/gitlab/jfronny/scripts/codegen/ClassGenerator.kt @@ -0,0 +1,127 @@ +package io.gitlab.jfronny.scripts.codegen + +class ClassGenerator(`package`: String, val name: String, val indent: String = " ") : Generator() { + override fun generateFinalized() = ensureBody { gen.appendLine("}").toString() } + private val gen: StringBuilder = StringBuilder("package $`package`;\n\n") + private var headerGenerated = false + private var extends: String? = null + private val implements: MutableList = ArrayList() + private var hadImport = false + + fun ensureHeader(action: () -> T): T = ensureMutable { + if (headerGenerated) throw IllegalAccessException("Attempted to generate pre-header statement while in body") + action() + } + + fun ensureBody(action: () -> T): T = ensureMutable { + if (!headerGenerated) { + headerGenerated = true + if (hadImport) gen.appendLine() + gen.append("public class $name") + if (extends != null) gen.append(" extends $extends") + if (implements.isNotEmpty()) gen.append(" implements ").append(implements.joinToString(", ") { it }) + gen.append(" {") + gen.appendLine() + } + action() + } + + fun rawHead(toWrite: String): Unit = ensureHeader { gen.appendLine(toWrite.trimIndent().trim('\n').prependIndent(indent)) } + fun rawBody(toWrite: String): Unit = ensureBody { gen.appendLine(toWrite.trimIndent().trim('\n').prependIndent(indent)) } + + fun import(`import`: String): Unit = ensureHeader { + check(import, "import", importPattern) + gen.appendLine("import $import;") + hadImport = true + } + + fun extends(extends: String): Unit = ensureHeader { + check(extends, "extended class", classPattern) + if (this.extends != null) throw IllegalAccessException("Attempted to extend multiple classes") + this.extends = extends + } + + fun implements(implements: String): Unit = ensureHeader { + check(implements, "implemented interface", classPattern) + this.implements.add(implements) + } + + fun fieldRaw(name: String, type: String, valueRaw: String, modifiers: String = defaultModifiers): Unit = ensureBody { + check(name, "field", classEntryPattern) + check(type, "field type", classPattern) + check(modifiers, "set of modifiers", modifierPattern) + gen.appendLine("$indent$modifiers $type $name = $valueRaw;") + } + + fun field(name: String, value: Int, modifiers: String = defaultModifiers) = fieldRaw(name, "int", value.toString(), modifiers) + fun field(name: String, value: Boolean, modifiers: String = defaultModifiers) = fieldRaw(name, "boolean", value.toString(), modifiers) + fun field(name: String, value: Double, modifiers: String = defaultModifiers) = fieldRaw(name, "double", value.toString(), modifiers) + fun field(name: String, value: Float, modifiers: String = defaultModifiers) = fieldRaw(name, "float", value.toString() + "f", modifiers) + fun field(name: String, value: String, modifiers: String = defaultModifiers) = fieldRaw(name, "String", enquote(value), modifiers) + + fun procedure(name: String, argument: Map, modifiers: String = defaultModifiers, body: String, throws: String? = null): Unit = ensureBody { + check(name, "procedure", classEntryPattern) + if (argument.any { !classEntryPattern.matches(it.key) || !classPattern.matches(it.value) }) throw IllegalArgumentException("invalid argument on procedure") + check(modifiers, "set of modifiers", modifierPattern) + ensureBrackets(body) + if (throws != null) check(throws, "thrown exceptions list", classListPattern) + gen.append(indent) + gen.append(modifiers) + gen.append(" void ") + gen.append(name) + gen.append("(") + gen.append(argument.entries.joinToString(", ") { "${it.key} ${it.value}" }) + gen.append(") ") + if (throws != null) gen.append(throws).append(' ') + gen.append('{') + gen.appendLine() + gen.appendLine(body.trimIndent().prependIndent("$indent ")) + gen.appendLine("$indent}") + } + + fun function(name: String, returnType: String, argument: Map, modifiers: String = defaultModifiers, body: String, throws: String? = null): Unit = ensureBody { + check(name, "procedure", classEntryPattern) + check(returnType, "return type", classPattern) + if (argument.any { !classEntryPattern.matches(it.key) || !classPattern.matches(it.value) }) throw IllegalArgumentException("invalid argument on procedure") + check(modifiers, "set of modifiers", modifierPattern) + ensureBrackets(body) + if (throws != null) check(throws, "thrown exceptions list", classListPattern) + gen.append(indent) + gen.append(modifiers) + gen.append(" $returnType ") + gen.append(name) + gen.append("(") + gen.append(argument.entries.joinToString(", ") { "${it.key} ${it.value}" }) + gen.append(") ") + if (throws != null) gen.append(throws).append(' ') + gen.append('{') + gen.appendLine() + gen.appendLine(body.trimIndent().prependIndent("$indent ")) + gen.appendLine("$indent}") + } + + companion object { + @JvmStatic private val defaultModifiers = "public static" + @JvmStatic private fun enquote(value: String): String = "\"${value.replace("\\", "\\\\").replace("\"", "\\\"")}\"" + @JvmStatic private fun ensureBrackets(value: String): Unit { + var state = 0 // 0 -> outside of anything, 1 -> inside string, 2 -> inside block quote + var brackets = 0 + value.forEachIndexed { i, c -> + when (state) { + 0 -> { + if (c == '"') state = if (i + 2 < value.length && value[i+1] == '"' && value[i+2] == '"') 2 else 1 + if (c == '{') brackets++ + if (c == '}') brackets-- + } + 1 -> { + if (c == '"') state = 0 + } + 2 -> { + if (c == '"' && i + 2 < value.length && value[i+1] == '"' && value[i+2] == '"') state = 0 + } + } + } + if (brackets != 0) throw IllegalArgumentException("Unclosed brackets in method body") + } + } +} diff --git a/convention/src/main/kotlin/io/gitlab/jfronny/scripts/codegen/ContentGenerator.kt b/convention/src/main/kotlin/io/gitlab/jfronny/scripts/codegen/ContentGenerator.kt new file mode 100644 index 0000000..2a23e1d --- /dev/null +++ b/convention/src/main/kotlin/io/gitlab/jfronny/scripts/codegen/ContentGenerator.kt @@ -0,0 +1,32 @@ +package io.gitlab.jfronny.scripts.codegen + +import org.gradle.api.Action +import java.util.function.Supplier + +class ContentGenerator : Generator() { + private val classes: MutableMap = LinkedHashMap() + private val resources: MutableMap = LinkedHashMap() + + override fun generateFinalized() = Generated(ImmutableMap(classes), ImmutableMap(resources)) + data class Generated(val classes: Map, val resources: Map) + + fun `class`(`package`: String, name: String, generator: Action) = ensureMutable { + if (!packagePattern.matches(`package`)) throw IllegalArgumentException("package \"$`package`\" is not a valid package") + if (!classNamePattern.matches(name)) throw IllegalArgumentException("class name \"$name\" is not a valid class name") + val builder = ClassGenerator(`package`, name) + generator.execute(builder) + classes["${`package`.replace('.', '/')}/$name.java"] = builder.finalize() + } + + fun text(path: String, generator: Action) = ensureMutable { + if (!pathPattern.matches(path)) throw IllegalArgumentException("path \"$path\" is not a valid path") + val builder = StringBuilder() + generator.execute(builder) + resources[path] = builder.toString().toByteArray() + } + + fun blob(path: String, generator: Supplier) = ensureMutable { + if (!pathPattern.matches(path)) throw IllegalArgumentException("path \"$path\" is not a valid path") + resources[path] = generator.get() + } +} \ No newline at end of file diff --git a/convention/src/main/kotlin/io/gitlab/jfronny/scripts/codegen/Generator.kt b/convention/src/main/kotlin/io/gitlab/jfronny/scripts/codegen/Generator.kt new file mode 100644 index 0000000..7f4a998 --- /dev/null +++ b/convention/src/main/kotlin/io/gitlab/jfronny/scripts/codegen/Generator.kt @@ -0,0 +1,36 @@ +package io.gitlab.jfronny.scripts.codegen + +abstract class Generator { + private var finalized = false + protected abstract fun generateFinalized(): T + + fun finalize(): T { + val fin = generateFinalized() + finalized = true + return fin + } + + protected fun ensureMutable(action: () -> T): T { + if (finalized) throw IllegalAccessException("Attempted to access CodeGenerator after it was finalized") + return action() + } + + protected data class ImmutableMap(private val inner: Map): Map by inner + + companion object { + @JvmStatic protected val primitivePattern = Regex("(?:byte|short|int|long|float|double|boolean|char)") + @JvmStatic protected val singleModifierPattern = Regex("(?:public|private|protected|static|final|abstract|transient|synchronized|volatile)") + @JvmStatic protected val modifierPattern = Regex("(?:$singleModifierPattern(?: $singleModifierPattern)*)") + + @JvmStatic protected val packagePattern = Regex("(?:[a-z][a-z0-9_]*(\\.[a-z][a-z0-9_]*)*)") + @JvmStatic protected val classNamePattern = Regex("(?:[A-Z][A-Za-z0-9_\$]*)") + @JvmStatic protected val pathPattern = Regex("(?:([a-z]+/)*[a-zA-Z][a-zA-Z0-9_.]*)") + @JvmStatic protected val classPattern = Regex("(?:(?:(?:$packagePattern\\.)?$classNamePattern)|$primitivePattern)") + @JvmStatic protected val importPattern = Regex("(?:$packagePattern\\.(?:\\*|$classNamePattern))") + @JvmStatic protected val classEntryPattern = Regex("(?:[a-zA-Z][a-zA-Z_\$0-9]*)") + @JvmStatic protected val classListPattern = Regex("(?:$classPattern(?:, $classPattern)*)") + @JvmStatic protected fun check(name: String, type: String, pattern: Regex): Unit { + if (!pattern.matches(name)) throw IllegalArgumentException("$type \"$name\" is not a valid $type") + } + } +} \ No newline at end of file diff --git a/convention/src/main/kotlin/jf.codegen.gradle.kts b/convention/src/main/kotlin/jf.codegen.gradle.kts new file mode 100644 index 0000000..9fa7853 --- /dev/null +++ b/convention/src/main/kotlin/jf.codegen.gradle.kts @@ -0,0 +1,35 @@ +import gradle.kotlin.dsl.accessors._72efc76fad8c8cf3476d335fb6323bde.* +import io.gitlab.jfronny.scripts.codegen.ContentGenerator.Generated +import io.gitlab.jfronny.scripts.* + +plugins { + java +} + +extra["codeGenerators"] = LinkedHashMap() + +val jfCodegen by tasks.registering { + doLast { + (project.extra["codeGenerators"] as LinkedHashMap).forEach { (name, generated) -> + generated.classes.forEach { (filePath, content) -> + val path = codegenDir.resolve("java").resolve(name).resolve(filePath) + path.parentFile.mkdirs() + path.writeText(content) + } + generated.resources.forEach { (filePath, content) -> + val path = codegenDir.resolve("resources").resolve(name).resolve(filePath) + path.parentFile.mkdirs() + path.writeBytes(content) + } + } + project.extra["codeGenerators"] = null + } +} + +tasks.compileJava { + dependsOn(jfCodegen.get()) +} + +tasks.processResources { + dependsOn(jfCodegen.get()) +} \ No newline at end of file