Skip to content
7 changes: 7 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -301,7 +301,14 @@ comma-separated options can be added to `--pbandk_out` before the colon and out
```
protoc --pbandk_out=kotlin_package=my.pkg:src/main/kotlin sample.proto
```

* If you have multiple proto packages, you can map them using `kotlin_package_mapping` option like so:

```
protoc --pbandk_out=kotlin_package_mapping="simple.package->new.package;foo.bar.*->my.foo.bar.*":src/main/kotlin sample.proto
```


* By default all generated classes have `public` visibility. To change the visibility to `internal`, use the
`visibility` option like so:

Expand Down
5 changes: 5 additions & 0 deletions buildSrc/src/main/kotlin/KotlinProtocTask.kt
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,10 @@ open class KotlinProtocTask : ProtocTask() {
@Optional
val kotlinPackage: Property<String> = project.objects.property()

@Input
@Optional
val kotlinPackageMapping: Property<String> = project.objects.property()

@Console
val logLevel: Property<String> = project.objects.property()

Expand All @@ -26,6 +30,7 @@ open class KotlinProtocTask : ProtocTask() {
it.resolve("bin/protoc-gen-pbandk" + ".bat".takeIf { OperatingSystem.current().isWindows }.orEmpty())
}))
pluginOptions.add(kotlinPackage.map { "kotlin_package" to it })
pluginOptions.add(kotlinPackageMapping.map { "kotlin_package_mapping" to it })

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I have a guess about why it's failing (I haven't tested this yet): I suspect that when kotlinPackageMapping is empty, this code will still add a null value to the pluginOptions list. When ProtocTask then runs protoc, it ends up passing a bogus value on the command line for the protoc-gen-pbandk plugin options.

This bug has always been here, even with the kotlinPackage line above this one. But it was never noticed because even though kotlinPackage is @Optional, every existing use of KotlinProtocTask provides a value for kotlinPackage.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It can be dumb but how about providing an empty string as the default value? I think it should convert to an empty list behind the curtains.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@garyp we are releasing 0.14.2 without this protoc task, would that be OK? What should we do for new proto options?

pluginOptions.add(logLevel.map { "log" to it })
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -234,18 +234,42 @@ internal open class FileBuilder(val namer: Namer = Namer.Standard, val supportMa

data class Context(val fileDesc: FileDescriptorProto, val params: Map<String, String>) {
// Support option kotlin_package_mapping=from.package1->to.package1;from.package2->to.package2
// or kotlin_package_mapping="from.*->to.*"
val packageMappings = params["kotlin_package_mapping"]
?.split(";")
?.associate { it.substringBefore("->") to it.substringAfter("->", "") }
?: emptyMap()

val kotlinPackageName = params["kotlin_package"]
?: fileDesc.options?.uninterpretedOption?.find {
it.name.singleOrNull()?.namePart == "kotlin_package"
}?.stringValue?.array?.decodeToString()
?: packageMappings[fileDesc.`package`]
?: fileDesc.options?.javaPackage?.takeIf { it.isNotEmpty() }
?: fileDesc.`package`?.takeIf { it.isNotEmpty() }

private fun getPackageName(): String? =
params["kotlin_package"]
?: fileDesc.options?.uninterpretedOption?.find {
it.name.singleOrNull()?.namePart == "kotlin_package"
}?.stringValue?.array?.decodeToString()
?: fileDesc.options?.javaPackage?.takeIf { it.isNotEmpty() }
?: fileDesc.`package`?.takeIf { it.isNotEmpty() }

private fun matchPackageNameFromPackageMappings(packageName: String): String? {
if (packageMappings[fileDesc.`package`] != null) return packageMappings[fileDesc.`package`]

return packageMappings
.filterKeys { it.endsWith("*") }
.firstNotNullOfOrNull { (from, to) ->
val prefixToMatch = from.substringBefore("*")
if (packageName.startsWith(prefixToMatch)) {
if (to.contains("*")) {
val prefixToReplaceWith = to.replace("*", "")
packageName.replaceFirst(prefixToMatch, prefixToReplaceWith)
} else {
to
}
} else {
null
}
}
}

val kotlinPackageName = getPackageName()?.let { matchPackageNameFromPackageMappings(it) ?: it }

fun findLocalMessage(name: String, parent: DescriptorProto? = null): DescriptorProto? {
// Get the set to look in and the type name
Expand Down
94 changes: 88 additions & 6 deletions protoc-gen-pbandk/lib/src/jvmTest/kotlin/CodeGeneratorTest.kt
Original file line number Diff line number Diff line change
Expand Up @@ -10,10 +10,7 @@ import java.io.File
import kotlin.reflect.full.declaredMemberProperties
import kotlin.reflect.full.hasAnnotation
import kotlin.reflect.full.memberProperties
import kotlin.test.Test
import kotlin.test.assertEquals
import kotlin.test.assertFalse
import kotlin.test.assertTrue
import kotlin.test.*

class CodeGeneratorTest {
private val descriptorSetOutput = File("build/generateTestProtoDescriptor/fileDescriptor.protoset")
Expand Down Expand Up @@ -76,11 +73,96 @@ class CodeGeneratorTest {
assertFalse("enum should not be nullable") { mainClazz.memberProperties.find { it.name == "enum" }!!.returnType.isMarkedNullable }
}

private fun compileProto(inputProto: String): KotlinCompilation.Result {
@Test
fun testKotlinPackageMappingSimple() {
val result = compileProto("simple.proto", "kotlin_package_mapping=foobar->newname.pkg")

assertEquals(ExitCode.OK, result.exitCode, result.messages)

// Ensure classes and fields were generated successfully
result.classLoader.loadClass("newname.pkg.Message1").kotlin
result.classLoader.loadClass("newname.pkg.Message2").kotlin

assertFails { result.classLoader.loadClass("foobar.Message1").kotlin }
}


@Test
fun testKotlinPackageMappingSimpleWildcard() {
val result = compileProto("simple.proto", "kotlin_package_mapping=*->newname.pkg.*")

assertEquals(ExitCode.OK, result.exitCode, result.messages)

// Ensure classes and fields were generated successfully
// New package name should prefix the existing one.
result.classLoader.loadClass("newname.pkg.foobar.Message1").kotlin
result.classLoader.loadClass("newname.pkg.foobar.Message2").kotlin

assertFails { result.classLoader.loadClass("foobar.Message1").kotlin }
}

@Test
fun testKotlinPackageMappingSimpleWildcardSingle() {
val result = compileProto("simple.proto", "kotlin_package_mapping=*->newname.pkg")

assertEquals(ExitCode.OK, result.exitCode, result.messages)

// Ensure classes and fields were generated successfully
// New package name should overwrite the old one
result.classLoader.loadClass("newname.pkg.Message1").kotlin
result.classLoader.loadClass("newname.pkg.Message2").kotlin

assertFails { result.classLoader.loadClass("foobar.Message1").kotlin }
}

@Test
fun testKotlinPackageMappingSimpleWildcardNoMismatch() {
val result = compileProto("simple.proto", "kotlin_package_mapping=foobar.*->newname.pkg")

assertEquals(ExitCode.OK, result.exitCode, result.messages)

// Ensure classes and fields were generated successfully
// New package name shouldn't change.
result.classLoader.loadClass("foobar.Message1").kotlin
result.classLoader.loadClass("foobar.Message2").kotlin


assertFails { result.classLoader.loadClass("newname.pkg.Message1").kotlin }
}

@Test
fun testKotlinPackageMappingWildcardPrefixMatch() {
val result = compileProto("proto_3_presence.proto", "kotlin_package_mapping=pbandk.*->newname.*")

assertEquals(ExitCode.OK, result.exitCode, result.messages)

// Ensure classes and fields were generated successfully
// New package name should replace the prefix.
result.classLoader.loadClass("newname.testpb.Proto3PresenceMessage").kotlin

assertFails { result.classLoader.loadClass("pbandk.testpb.Proto3PresenceMessage").kotlin }
}

@Test
fun testKotlinPackageMappingWithJavaPackage() {
val result = compileProto("with_java_package.proto", "kotlin_package_mapping=pbandk.*->newname.*")

assertEquals(ExitCode.OK, result.exitCode, result.messages)

// Ensure classes and fields were generated successfully
// New package name should contain
result.classLoader.loadClass("newname.javapackage.Foo").kotlin
result.classLoader.loadClass("newname.javapackage.Goo").kotlin

assertFails { result.classLoader.loadClass("pbandk.javapackage.Foo").kotlin }
}

private fun compileProto(inputProto: String, parameter: String? = null): KotlinCompilation.Result {
val gen = runGenerator(
CodeGeneratorRequest(
fileToGenerate = listOf(inputProto),
protoFile = fileDescriptorSet
protoFile = fileDescriptorSet,
parameter = parameter,
)
)

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
syntax = "proto3";

package pbandk.oldpackage;

option java_package = "pbandk.javapackage";

message Foo {
int32 bar = 1;
}

message Goo {
Foo foo = 1;
}