diff --git a/README.md b/README.md index bf51709f..16086579 100644 --- a/README.md +++ b/README.md @@ -318,6 +318,19 @@ comma-separated options can be added to `--pbandk_out` before the colon and out * To log debug logs during generation, `log=debug` can be set as well. +* **EXPERIMENTAL**: To generate code with not-null (required) message types, you can use a custom boolean option + by extending `google.protobuf.FieldOptions` to include a `bool` field such as `is_required` and then setting + `required_field_extension_number`. + + Note that this will invoke the default constructor for messages so if there is a circular dependency, + it is user's responsibility to omit it. + + ``` + protoc --pbandk_out=required_field_extension_number=50008:src/main/kotlin sample.proto + ``` + + You can refer to `protoc-gen-pbandk/lib/src/jvmTest/resources/protos/custom_options.proto` as an example. + Multiple options can be added to a single `--pbandk_out` argument by separating them with commas. In addition to running `protoc` manually, the diff --git a/protoc-gen-pbandk/lib/src/commonMain/kotlin/pbandk/gen/CodeGenerator.kt b/protoc-gen-pbandk/lib/src/commonMain/kotlin/pbandk/gen/CodeGenerator.kt index 31bc1acd..c33c2876 100644 --- a/protoc-gen-pbandk/lib/src/commonMain/kotlin/pbandk/gen/CodeGenerator.kt +++ b/protoc-gen-pbandk/lib/src/commonMain/kotlin/pbandk/gen/CodeGenerator.kt @@ -9,6 +9,7 @@ public open class CodeGenerator( public val params: Map ) { protected val visibility: String = params["visibility"] ?: "public" + protected val requiredFieldNumber: Int? = params["required_field_extension_number"]?.toIntOrNull() protected val bld: StringBuilder = StringBuilder() protected var indent: String = "" @@ -39,7 +40,7 @@ public open class CodeGenerator( addDeprecatedAnnotation(field) line( "val ${field.extendeeKotlinType}.${field.kotlinFieldName}: ${ - field.kotlinValueType(true) + field.kotlinValueType() } " ).indented { line("get() = getExtension(${file.kotlinPackageName}.${field.kotlinFieldName})") @@ -155,7 +156,7 @@ public open class CodeGenerator( if (field is File.Field.Numbered.Standard && field.required) { lineMid("val ${field.kotlinFieldName}: ${field.kotlinValueType(false)}") } else { - lineMid("val ${field.kotlinFieldName}: ${field.kotlinValueType(true)}") + lineMid("val ${field.kotlinFieldName}: ${field.kotlinValueType()}") lineMid(" = ${field.defaultValue}") } return this @@ -507,7 +508,12 @@ public open class CodeGenerator( protected val File.Type.Message.mapEntryValueKotlinType: String? get() = if (!mapEntry) null else (fields[1] as File.Field.Numbered.Standard).kotlinValueType(true) - protected fun File.Field.Numbered.kotlinValueType(allowNulls: Boolean): String = when (this) { + protected val File.Field.Numbered.allowNulls: Boolean + get() = requiredFieldNumber?.let { + // 1 means `required = true` so we can't allow nulls + this.options.unknownFields[it]?.values?.get(0)?.rawBytes?.array?.get(0)?.toInt() != 1 + } ?: true + protected fun File.Field.Numbered.kotlinValueType(allowNulls: Boolean = this.allowNulls): String = when (this) { is File.Field.Numbered.Standard -> kotlinValueType(allowNulls) is File.Field.Numbered.Wrapper -> kotlinValueType(allowNulls) } @@ -558,7 +564,7 @@ public open class CodeGenerator( else "var $kotlinFieldName: pbandk.MessageMap.Builder<" + "${mapEntry.mapEntryKeyKotlinType}, ${mapEntry.mapEntryValueKotlinType}>? = null" } - requiresExplicitTypeWithVal -> "var $kotlinFieldName: ${kotlinValueType(true)} = ${defaultValue()}" + requiresExplicitTypeWithVal -> "var $kotlinFieldName: ${kotlinValueType()} = ${defaultValue()}" else -> "var $kotlinFieldName = ${defaultValue()}" } protected val File.Field.Numbered.Standard.decodeWithVarDone: String @@ -576,12 +582,12 @@ public open class CodeGenerator( else -> kotlinQualifiedTypeName } - protected fun File.Field.Numbered.Standard.defaultValue(allowNulls: Boolean = true): String = when { + protected fun File.Field.Numbered.Standard.defaultValue(allowNulls: Boolean = this.allowNulls): String = when { map -> "emptyMap()" repeated -> "emptyList()" allowNulls && hasPresence -> "null" type == File.Field.Type.ENUM -> "$kotlinQualifiedTypeName.fromValue(0)" - else -> type.defaultValue + else -> type.defaultValue(allowNulls, kotlinQualifiedTypeName) } protected val File.Field.Numbered.Standard.requiresExplicitTypeWithVal: Boolean get() = repeated || hasPresence || type.requiresExplicitTypeWithVal @@ -648,8 +654,7 @@ public open class CodeGenerator( File.Field.Type.UINT32 -> "Int" File.Field.Type.UINT64 -> "Long" } - protected val File.Field.Type.defaultValue: String - get() = when (this) { + protected fun File.Field.Type.defaultValue(allowNulls: Boolean, kotlinQualifiedTypeName: String): String = when (this) { File.Field.Type.BOOL -> "false" File.Field.Type.BYTES -> "pbandk.ByteArr.empty" File.Field.Type.DOUBLE -> "0.0" @@ -659,7 +664,7 @@ public open class CodeGenerator( File.Field.Type.FIXED64, File.Field.Type.INT64, File.Field.Type.SFIXED64, File.Field.Type.SINT64, File.Field.Type.UINT64 -> "0L" File.Field.Type.FLOAT -> "0.0F" - File.Field.Type.MESSAGE -> "null" + File.Field.Type.MESSAGE -> if (allowNulls) "null" else "$kotlinQualifiedTypeName()" File.Field.Type.STRING -> "\"\"" } protected val File.Field.Type.requiresExplicitTypeWithVal: Boolean diff --git a/protoc-gen-pbandk/lib/src/jvmTest/kotlin/CodeGeneratorTest.kt b/protoc-gen-pbandk/lib/src/jvmTest/kotlin/CodeGeneratorTest.kt index e950f8b4..004bd3fa 100644 --- a/protoc-gen-pbandk/lib/src/jvmTest/kotlin/CodeGeneratorTest.kt +++ b/protoc-gen-pbandk/lib/src/jvmTest/kotlin/CodeGeneratorTest.kt @@ -46,6 +46,29 @@ class CodeGeneratorTest { assertTrue(deprecatedField.hasAnnotation()) } + @Test + fun testCustomOptionsWithExtensionNumber() { + val result = compileProto("custom_options.proto", "required_field_extension_number=50008") + + assertEquals(ExitCode.OK, result.exitCode, result.messages) + val fooClazz = result.classLoader.loadClass("foobar.RequestType").kotlin + + val requiredField = fooClazz.declaredMemberProperties.single { it.name == "myRequiredField" } + assertFalse(requiredField.returnType.isMarkedNullable) + } + + + @Test + fun testCustomOptionsWithoutExtensionNumber() { + val result = compileProto("custom_options.proto") + + assertEquals(ExitCode.OK, result.exitCode, result.messages) + val fooClazz = result.classLoader.loadClass("foobar.RequestType").kotlin + + val requiredField = fooClazz.declaredMemberProperties.single { it.name == "myRequiredField" } + assertTrue(requiredField.returnType.isMarkedNullable) + } + @Test fun testOneOf_SameNameField() { val result = compileProto("oneof_same_name.proto") diff --git a/protoc-gen-pbandk/lib/src/jvmTest/resources/protos/custom_options.proto b/protoc-gen-pbandk/lib/src/jvmTest/resources/protos/custom_options.proto new file mode 100644 index 00000000..a4d8bdba --- /dev/null +++ b/protoc-gen-pbandk/lib/src/jvmTest/resources/protos/custom_options.proto @@ -0,0 +1,73 @@ +// Examples taken from https://developers.google.com/protocol-buffers/docs/proto#customoptions +syntax = "proto3"; +package foobar; + +import "google/protobuf/descriptor.proto"; + +extend google.protobuf.FileOptions { + optional string my_file_option = 50000; +} +extend google.protobuf.MessageOptions { + optional int32 my_message_option = 50001; +} +extend google.protobuf.FieldOptions { + optional float my_field_option = 50002; + optional bool is_required = 50008; +} +extend google.protobuf.OneofOptions { + optional int64 my_oneof_option = 50003; +} +extend google.protobuf.EnumOptions { + optional bool my_enum_option = 50004; +} +extend google.protobuf.EnumValueOptions { + optional uint32 my_enum_value_option = 50005; +} +extend google.protobuf.ServiceOptions { + optional MyEnum my_service_option = 50006; +} +extend google.protobuf.MethodOptions { + optional MyMessage my_method_option = 50007; +} + +option (my_file_option) = "Hello world!"; + +message MyMessage { + option (my_message_option) = 1234; + + optional int32 foo = 1 [(my_field_option) = 4.5]; + optional string bar = 2; + oneof qux { + option (my_oneof_option) = 42; + + string quux = 3; + } +} + +enum MyEnum { + option (my_enum_option) = true; + + FOO = 0 [(my_enum_value_option) = 321]; + BAR = 1; +} + +message RequestType { + MyMessage my_not_required_field = 1 [(is_required) = false]; + MyMessage my_required_field = 2 [(is_required) = true]; + repeated MyMessage my_not_required_fields = 3 [(is_required) = false]; + repeated MyMessage my_required_fields = 4 [(is_required) = true]; + map my_not_required_map = 5 [(is_required) = false]; + map my_required_map = 6 [(is_required) = true]; +} +message ResponseType {} + +service MyService { + option (my_service_option) = FOO; + + rpc MyMethod(RequestType) returns(ResponseType) { + // Note: my_method_option has type MyMessage. We can set each field + // within it using a separate "option" line. + option (my_method_option).foo = 567; + option (my_method_option).bar = "Some string"; + } +} diff --git a/protoc-gen-pbandk/lib/src/jvmTest/resources/protos/options.proto b/protoc-gen-pbandk/lib/src/jvmTest/resources/protos/options.proto index 9c4b418a..284fbd15 100644 --- a/protoc-gen-pbandk/lib/src/jvmTest/resources/protos/options.proto +++ b/protoc-gen-pbandk/lib/src/jvmTest/resources/protos/options.proto @@ -3,4 +3,4 @@ package foobar; message Foo { int32 deprecated_field = 1 [deprecated = true]; -} \ No newline at end of file +} diff --git a/protoc-gen-pbandk/lib/src/jvmTest/resources/protos/simple.proto b/protoc-gen-pbandk/lib/src/jvmTest/resources/protos/simple.proto index 9930cfd6..85c65ff9 100644 --- a/protoc-gen-pbandk/lib/src/jvmTest/resources/protos/simple.proto +++ b/protoc-gen-pbandk/lib/src/jvmTest/resources/protos/simple.proto @@ -7,4 +7,4 @@ message Message1 { message Message2 { string str_val = 1; -} \ No newline at end of file +}