diff --git a/README.md b/README.md index ec4f5362..e65758c7 100644 --- a/README.md +++ b/README.md @@ -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: diff --git a/buildSrc/src/main/kotlin/KotlinProtocTask.kt b/buildSrc/src/main/kotlin/KotlinProtocTask.kt index fbada845..5a317064 100644 --- a/buildSrc/src/main/kotlin/KotlinProtocTask.kt +++ b/buildSrc/src/main/kotlin/KotlinProtocTask.kt @@ -12,6 +12,10 @@ open class KotlinProtocTask : ProtocTask() { @Optional val kotlinPackage: Property = project.objects.property() + @Input + @Optional + val kotlinPackageMapping: Property = project.objects.property() + @Console val logLevel: Property = project.objects.property() @@ -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 }) pluginOptions.add(logLevel.map { "log" to it }) } } diff --git a/protoc-gen-pbandk/lib/src/commonMain/kotlin/pbandk/gen/FileBuilder.kt b/protoc-gen-pbandk/lib/src/commonMain/kotlin/pbandk/gen/FileBuilder.kt index f1a2eed9..f6a06a9f 100644 --- a/protoc-gen-pbandk/lib/src/commonMain/kotlin/pbandk/gen/FileBuilder.kt +++ b/protoc-gen-pbandk/lib/src/commonMain/kotlin/pbandk/gen/FileBuilder.kt @@ -234,18 +234,42 @@ internal open class FileBuilder(val namer: Namer = Namer.Standard, val supportMa data class Context(val fileDesc: FileDescriptorProto, val params: Map) { // 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 diff --git a/protoc-gen-pbandk/lib/src/jvmTest/kotlin/CodeGeneratorTest.kt b/protoc-gen-pbandk/lib/src/jvmTest/kotlin/CodeGeneratorTest.kt index ae2c5d3b..c4f01852 100644 --- a/protoc-gen-pbandk/lib/src/jvmTest/kotlin/CodeGeneratorTest.kt +++ b/protoc-gen-pbandk/lib/src/jvmTest/kotlin/CodeGeneratorTest.kt @@ -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") @@ -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, ) ) diff --git a/protoc-gen-pbandk/lib/src/jvmTest/resources/protos/with_java_package.proto b/protoc-gen-pbandk/lib/src/jvmTest/resources/protos/with_java_package.proto new file mode 100644 index 00000000..3b98c85e --- /dev/null +++ b/protoc-gen-pbandk/lib/src/jvmTest/resources/protos/with_java_package.proto @@ -0,0 +1,13 @@ +syntax = "proto3"; + +package pbandk.oldpackage; + +option java_package = "pbandk.javapackage"; + +message Foo { + int32 bar = 1; +} + +message Goo { + Foo foo = 1; +}