Skip to content

Commit 32bbabb

Browse files
committed
Protobuf plugin
1 parent b390f39 commit 32bbabb

28 files changed

+1574
-0
lines changed
Lines changed: 307 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,307 @@
1+
/*
2+
* Copyright 2023-2025 JetBrains s.r.o and contributors. Use of this source code is governed by the Apache 2.0 license.
3+
*/
4+
5+
package kotlinx.rpc.protobuf
6+
7+
import org.slf4j.Logger
8+
import org.slf4j.helpers.NOPLogger
9+
10+
data class CodeGenerationParameters(
11+
val messageMode: RpcProtobufPlugin.MessageMode,
12+
)
13+
14+
open class CodeGenerator(
15+
val parameters: CodeGenerationParameters,
16+
private val indent: String,
17+
private val builder: StringBuilder = StringBuilder(),
18+
private val logger: Logger = NOPLogger.NOP_LOGGER,
19+
) {
20+
private var isEmpty: Boolean = true
21+
private var result: String? = null
22+
private var lastIsDeclaration: Boolean = false
23+
24+
@Suppress("FunctionName")
25+
private fun _append(
26+
value: String? = null,
27+
newLineBefore: Boolean = false,
28+
newLineAfter: Boolean = false,
29+
newLineIfAbsent: Boolean = false,
30+
) {
31+
var addedNewLineBefore = false
32+
33+
if (lastIsDeclaration) {
34+
builder.appendLine()
35+
lastIsDeclaration = false
36+
addedNewLineBefore = true
37+
} else if (newLineIfAbsent) {
38+
val last = builder.lastOrNull()
39+
if (last != null && last != '\n') {
40+
builder.appendLine()
41+
addedNewLineBefore = true
42+
}
43+
}
44+
45+
if (!addedNewLineBefore && newLineBefore) {
46+
builder.appendLine()
47+
}
48+
if (value != null) {
49+
builder.append(value)
50+
}
51+
if (newLineAfter) {
52+
builder.appendLine()
53+
}
54+
55+
isEmpty = false
56+
}
57+
58+
private fun append(value: String) {
59+
_append(value)
60+
}
61+
62+
private fun addLine(value: String? = null) {
63+
_append("$indent${value ?: ""}", newLineIfAbsent = true)
64+
}
65+
66+
fun newLine() {
67+
_append(newLineBefore = true)
68+
}
69+
70+
private fun withNextIndent(block: CodeGenerator.() -> Unit) {
71+
CodeGenerator(parameters, "$indent$ONE_INDENT", builder, logger).block()
72+
}
73+
74+
internal fun scope(prefix: String, suffix: String = "", block: (CodeGenerator.() -> Unit)? = null) {
75+
addLine(prefix)
76+
scopeWithSuffix(suffix, block)
77+
}
78+
79+
private fun scopeWithSuffix(suffix: String = "", block: (CodeGenerator.() -> Unit)? = null) {
80+
if (block == null) {
81+
newLine()
82+
lastIsDeclaration = true
83+
return
84+
}
85+
86+
val nested = CodeGenerator(parameters, "$indent$ONE_INDENT", logger = logger).apply(block)
87+
88+
if (nested.isEmpty) {
89+
newLine()
90+
lastIsDeclaration = true
91+
return
92+
}
93+
94+
append(" {")
95+
newLine()
96+
append(nested.build().trimEnd())
97+
addLine("}$suffix")
98+
newLine()
99+
lastIsDeclaration = true
100+
}
101+
102+
fun code(code: String) {
103+
code.lines().forEach { addLine(it) }
104+
}
105+
106+
fun property(
107+
name: String,
108+
modifiers: String = "",
109+
contextReceiver: String = "",
110+
type: String = "",
111+
delegate: Boolean = false,
112+
value: String = "",
113+
block: (CodeGenerator.() -> Unit)? = null,
114+
) {
115+
val modifiersString = if (modifiers.isEmpty()) "" else "$modifiers "
116+
val contextString = if (contextReceiver.isEmpty()) "" else "$contextReceiver."
117+
val typeString = if (type.isEmpty()) "" else ": $type"
118+
val delegateString = if (delegate) " by " else " = "
119+
scope("${modifiersString}val $contextString$name$typeString$delegateString$value", block = block)
120+
}
121+
122+
fun function(
123+
name: String,
124+
modifiers: String = "",
125+
typeParameters: String = "",
126+
args: String = "",
127+
contextReceiver: String = "",
128+
returnType: String = "",
129+
block: (CodeGenerator.() -> Unit)? = null,
130+
) {
131+
val modifiersString = if (modifiers.isEmpty()) "" else "$modifiers "
132+
val contextString = if (contextReceiver.isEmpty()) "" else "$contextReceiver."
133+
val returnTypeString = if (returnType.isEmpty()) "" else ": $returnType"
134+
val typeParametersString = if (typeParameters.isEmpty()) "" else " <$typeParameters>"
135+
scope("${modifiersString}fun$typeParametersString $contextString$name($args)$returnTypeString", block = block)
136+
}
137+
138+
enum class DeclarationType(val strValue: String) {
139+
Class("class"), Interface("interface"), Object("object");
140+
}
141+
142+
@JvmName("clazz_no_constructorArgs")
143+
fun clazz(
144+
name: String,
145+
modifiers: String = "",
146+
superTypes: List<String> = emptyList(),
147+
annotations: List<String> = emptyList(),
148+
declarationType: DeclarationType = DeclarationType.Class,
149+
block: (CodeGenerator.() -> Unit)? = null,
150+
) {
151+
clazz(
152+
name = name,
153+
modifiers = modifiers,
154+
constructorArgs = emptyList<String>(),
155+
superTypes = superTypes,
156+
annotations = annotations,
157+
declarationType = declarationType,
158+
block = block,
159+
)
160+
}
161+
162+
@JvmName("clazz_constructorArgs_no_default")
163+
fun clazz(
164+
name: String,
165+
modifiers: String = "",
166+
constructorArgs: List<String> = emptyList(),
167+
superTypes: List<String> = emptyList(),
168+
annotations: List<String> = emptyList(),
169+
declarationType: DeclarationType = DeclarationType.Class,
170+
block: (CodeGenerator.() -> Unit)? = null,
171+
) {
172+
clazz(
173+
name = name,
174+
modifiers = modifiers,
175+
constructorArgs = constructorArgs.map { it to null },
176+
superTypes = superTypes,
177+
annotations = annotations,
178+
declarationType = declarationType,
179+
block = block,
180+
)
181+
}
182+
183+
fun clazz(
184+
name: String,
185+
modifiers: String = "",
186+
constructorArgs: List<Pair<String, String?>> = emptyList(),
187+
superTypes: List<String> = emptyList(),
188+
annotations: List<String> = emptyList(),
189+
declarationType: DeclarationType = DeclarationType.Class,
190+
block: (CodeGenerator.() -> Unit)? = null,
191+
) {
192+
for (annotation in annotations) {
193+
addLine(annotation)
194+
}
195+
196+
val modifiersString = if (modifiers.isEmpty()) "" else "$modifiers "
197+
198+
val firstLine = "$modifiersString${declarationType.strValue}${if (name.isNotEmpty()) " " else ""}$name"
199+
addLine(firstLine)
200+
201+
val shouldPutArgsOnNewLines =
202+
firstLine.length + constructorArgs.sumOf {
203+
it.first.length + (it.second?.length?.plus(3) ?: 0) + 2
204+
} + indent.length > 80
205+
206+
val constructorArgsTransformed = constructorArgs.map { (arg, default) ->
207+
val defaultString = default?.let { " = $it" } ?: ""
208+
"$arg$defaultString"
209+
}
210+
211+
when {
212+
shouldPutArgsOnNewLines && constructorArgsTransformed.isNotEmpty() -> {
213+
append("(")
214+
newLine()
215+
withNextIndent {
216+
for (arg in constructorArgsTransformed) {
217+
addLine("$arg,")
218+
}
219+
}
220+
addLine(")")
221+
}
222+
223+
constructorArgsTransformed.isNotEmpty() -> {
224+
append("(")
225+
append(constructorArgsTransformed.joinToString(", "))
226+
append(")")
227+
}
228+
}
229+
230+
val superString = superTypes
231+
.takeIf { it.isNotEmpty() }
232+
?.joinToString(", ")
233+
?.let { ": $it" }
234+
?: ""
235+
236+
append(superString)
237+
238+
scopeWithSuffix(block = block)
239+
}
240+
241+
open fun build(): String {
242+
if (result == null) {
243+
result = builder.toString()
244+
}
245+
246+
return result!!
247+
}
248+
249+
companion object {
250+
private const val ONE_INDENT = " "
251+
}
252+
}
253+
254+
class FileGenerator(
255+
codeGenerationParameters: CodeGenerationParameters,
256+
var filename: String? = null,
257+
var packageName: String? = null,
258+
var fileOptIns: List<String> = emptyList(),
259+
logger: Logger = NOPLogger.NOP_LOGGER,
260+
) : CodeGenerator(codeGenerationParameters, "", logger = logger) {
261+
private val imports = mutableListOf<String>()
262+
263+
fun importPackage(name: String) {
264+
if (name != packageName) {
265+
imports.add("$name.*")
266+
}
267+
}
268+
269+
fun import(name: String) {
270+
imports.add(name)
271+
}
272+
273+
override fun build(): String {
274+
val sortedImports = imports.toSortedSet()
275+
val prefix = buildString {
276+
if (fileOptIns.isNotEmpty()) {
277+
appendLine("@file:OptIn(${fileOptIns.joinToString(", ")})")
278+
newLine()
279+
}
280+
281+
var packageName = packageName
282+
if (packageName != null && packageName.isNotEmpty()) {
283+
appendLine("package $packageName")
284+
}
285+
286+
appendLine()
287+
288+
for (import in sortedImports) {
289+
appendLine("import $import")
290+
}
291+
292+
if (imports.isNotEmpty()) {
293+
appendLine()
294+
}
295+
}
296+
297+
return prefix + super.build()
298+
}
299+
}
300+
301+
fun file(
302+
codeGenerationParameters: CodeGenerationParameters,
303+
name: String? = null,
304+
packageName: String? = null,
305+
logger: Logger = NOPLogger.NOP_LOGGER,
306+
block: FileGenerator.() -> Unit,
307+
): FileGenerator = FileGenerator(codeGenerationParameters, name, packageName, emptyList(), logger).apply(block)
Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
/*
2+
* Copyright 2023-2025 JetBrains s.r.o and contributors. Use of this source code is governed by the Apache 2.0 license.
3+
*/
4+
5+
package kotlinx.rpc.protobuf
6+
7+
import com.google.protobuf.compiler.PluginProtos
8+
9+
// todo
10+
// type resolution (avoid over qualified types)
11+
// comments
12+
// extensions
13+
// maps
14+
// kmp sources sets
15+
// platform specific bindings
16+
// common API
17+
// DSL builders
18+
// kotlin_multiple_files, kotlin_package options
19+
// library proto files
20+
// explicit API mode
21+
// services
22+
// unfolded types overloads
23+
// nested streams
24+
//
25+
26+
fun main() {
27+
val inputBytes = System.`in`.readBytes()
28+
val request = PluginProtos.CodeGeneratorRequest.parseFrom(inputBytes)
29+
val plugin = RpcProtobufPlugin()
30+
val output: PluginProtos.CodeGeneratorResponse = plugin.run(request)
31+
output.writeTo(System.out)
32+
}

0 commit comments

Comments
 (0)