Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 13 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ public open class CodeGenerator(
public val params: Map<String, String>
) {
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 = ""
Expand Down Expand Up @@ -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})")
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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) {
Comment on lines +511 to +516

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

what do you think about putting something like

val isRequired: Boolean
            get() = rawBytes.array.first().toInt() != 1

in the UnknownField.Value class and then having

options.unknownFields[it]?.values?.firstOrNull()?.isRequired == true

here?

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.

Maybe not isRequired because it is just a boolean. We need to smart cast those bytes to a boolean, maybe something like asBoolean() is a better candidate.

is File.Field.Numbered.Standard -> kotlinValueType(allowNulls)
is File.Field.Numbered.Wrapper -> kotlinValueType(allowNulls)
}
Expand Down Expand Up @@ -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
Expand All @@ -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
Expand Down Expand Up @@ -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"
Expand All @@ -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
Expand Down
23 changes: 23 additions & 0 deletions protoc-gen-pbandk/lib/src/jvmTest/kotlin/CodeGeneratorTest.kt
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,29 @@ class CodeGeneratorTest {
assertTrue(deprecatedField.hasAnnotation<Deprecated>())
}

@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")
Expand Down
Original file line number Diff line number Diff line change
@@ -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<int32, MyMessage> my_not_required_map = 5 [(is_required) = false];
map<int32, MyMessage> 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";
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -3,4 +3,4 @@ package foobar;

message Foo {
int32 deprecated_field = 1 [deprecated = true];
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -7,4 +7,4 @@ message Message1 {

message Message2 {
string str_val = 1;
}
}