Skip to content

Commit f00caa4

Browse files
authored
implement proto3 field presence (#88)
1 parent 7e7dade commit f00caa4

File tree

13 files changed

+143
-24
lines changed

13 files changed

+143
-24
lines changed

protokt-codegen/src/main/kotlin/com/toasttab/protokt/Main.kt

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ import com.google.protobuf.DescriptorProtos.FileDescriptorProto
2323
import com.google.protobuf.ExtensionRegistry
2424
import com.google.protobuf.compiler.PluginProtos.CodeGeneratorRequest
2525
import com.google.protobuf.compiler.PluginProtos.CodeGeneratorResponse
26+
import com.google.protobuf.compiler.PluginProtos.CodeGeneratorResponse.Feature
2627
import com.toasttab.protokt.codegen.generate
2728
import com.toasttab.protokt.codegen.impl.STAnnotator
2829
import com.toasttab.protokt.codegen.impl.STEffects
@@ -51,6 +52,7 @@ internal fun main(bytes: ByteArray, out: OutputStream) {
5152

5253
if (files.nonEmpty()) {
5354
CodeGeneratorResponse.newBuilder()
55+
.setSupportedFeatures(Feature.FEATURE_PROTO3_OPTIONAL.number.toLong())
5456
.addAllFile(files)
5557
.build()
5658
.writeTo(out)

protokt-codegen/src/main/kotlin/com/toasttab/protokt/codegen/impl/MessageAnnotator.kt

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@ import com.toasttab.protokt.codegen.impl.Implements.doesImplement
2424
import com.toasttab.protokt.codegen.impl.Implements.implements
2525
import com.toasttab.protokt.codegen.impl.MapEntryAnnotator.Companion.annotateMapEntry
2626
import com.toasttab.protokt.codegen.impl.MessageDocumentationAnnotator.annotateMessageDocumentation
27-
import com.toasttab.protokt.codegen.impl.OneofAnnotator.Companion.annotateOneOfs
27+
import com.toasttab.protokt.codegen.impl.OneofAnnotator.Companion.annotateOneofs
2828
import com.toasttab.protokt.codegen.impl.PropertyAnnotator.Companion.annotateProperties
2929
import com.toasttab.protokt.codegen.impl.STAnnotator.Context
3030
import com.toasttab.protokt.codegen.impl.STAnnotator.annotate
@@ -49,7 +49,7 @@ internal object MessageAnnotator {
4949
MessageTemplate.render(
5050
message = messageInfo(msg, ctx),
5151
properties = annotateProperties(msg, ctx),
52-
oneofs = annotateOneOfs(msg, ctx),
52+
oneofs = annotateOneofs(msg, ctx),
5353
sizeof = annotateSizeof(msg, ctx),
5454
serialize = annotateSerializer(msg, ctx),
5555
deserialize = annotateDeserializer(msg, ctx),

protokt-codegen/src/main/kotlin/com/toasttab/protokt/codegen/impl/Nullability.kt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,7 @@ internal object Nullability {
3636
private val Field.isKotlinRepresentationNullable
3737
get() =
3838
when (this) {
39-
is StandardField -> type == FieldType.MESSAGE && !repeated
39+
is StandardField -> (type == FieldType.MESSAGE && !repeated) || optional
4040
is Oneof -> true
4141
}
4242

protokt-codegen/src/main/kotlin/com/toasttab/protokt/codegen/impl/OneofAnnotator.kt

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,7 @@ private constructor(
3434
private val msg: Message,
3535
private val ctx: Context
3636
) {
37-
private fun annotateOneOfs() =
37+
private fun annotateOneofs() =
3838
msg.fields.map {
3939
when (it) {
4040
is Oneof ->
@@ -152,7 +152,7 @@ private constructor(
152152
msg.fields.filterIsInstance<Oneof>().map { it.name }
153153

154154
companion object {
155-
fun annotateOneOfs(msg: Message, ctx: Context) =
156-
OneofAnnotator(msg, ctx).annotateOneOfs()
155+
fun annotateOneofs(msg: Message, ctx: Context) =
156+
OneofAnnotator(msg, ctx).annotateOneofs()
157157
}
158158
}

protokt-codegen/src/main/kotlin/com/toasttab/protokt/codegen/impl/PropertyAnnotator.kt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -59,7 +59,7 @@ private constructor(
5959
fieldType = it.type.toString(),
6060
repeated = it.repeated,
6161
map = it.map,
62-
nullable = it.nullable,
62+
nullable = it.nullable || it.optional,
6363
nonNullOption = it.hasNonNullOption,
6464
overrides = it.overrides(ctx, msg),
6565
wrapped = it.wrapped,

protokt-codegen/src/main/kotlin/com/toasttab/protokt/codegen/protoc/Protocol.kt

Lines changed: 13 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -48,7 +48,6 @@ fun toProtocol(ctx: ProtocolContext) =
4848
FileDesc(
4949
name = ctx.fdp.name,
5050
packageName = ctx.fdp.`package`,
51-
version = ctx.fdp.syntax?.removePrefix("proto")?.toIntOrNull() ?: 2,
5251
options = ctx.fdp.fileOptions,
5352
context = ctx,
5453
sourceCodeInfo = ctx.fdp.sourceCodeInfo
@@ -270,8 +269,13 @@ private fun toOneof(
270269
field: FieldDescriptorProto,
271270
typeNames: Set<String>,
272271
fields: ImmutableList<Field>
273-
): Oneof {
272+
): Field {
274273
val newName = newFieldName(oneof.name, typeNames)
274+
275+
if (field.proto3Optional) {
276+
return toStandard(idx, ctx, field, typeNames)
277+
}
278+
275279
val standardTuple = desc.fieldList.filter {
276280
it.hasOneofIndex() && it.oneofIndex == field.oneofIndex
277281
}.foldIndexed(
@@ -309,22 +313,25 @@ private fun toStandard(
309313
usedFieldNames: Set<String>,
310314
alwaysRequired: Boolean = false
311315
): StandardField =
312-
toFieldType(checkNotNull(fdp.type) { "Missing field type" }).let { type ->
316+
toFieldType(fdp.type).let { type ->
313317
StandardField(
314318
number = fdp.number,
315319
name = newFieldName(fdp.name, usedFieldNames),
316320
type = type,
317321
repeated = fdp.label == LABEL_REPEATED,
318-
optional = !alwaysRequired && fdp.label == LABEL_OPTIONAL,
322+
optional =
323+
!alwaysRequired &&
324+
(fdp.label == LABEL_OPTIONAL && ctx.proto2) ||
325+
fdp.proto3Optional,
319326
packed =
320327
type.packable &&
321328
// marginal support for proto2
322-
((ctx.fdp.syntax == "proto2" && fdp.options.packed) ||
329+
((ctx.proto2 && fdp.options.packed) ||
323330
// packed if: proto3 and `packed` isn't set, or proto3
324331
// and `packed` is true. If proto3, only explicitly
325332
// setting `packed` to false disables packing, since
326333
// the default value for an unset boolean is false.
327-
(ctx.fdp.syntax == "proto3" &&
334+
(ctx.proto3 &&
328335
(!fdp.options.hasPacked() ||
329336
(fdp.options.hasPacked() && fdp.options.packed)))),
330337
mapEntry =

protokt-codegen/src/main/kotlin/com/toasttab/protokt/codegen/protoc/ProtocolContext.kt

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,9 @@ class ProtocolContext(
3535

3636
val fileName = fdp.name
3737
val version = getProtoktVersion(ProtocolContext::class)
38+
39+
val proto2 = !fdp.hasSyntax() || fdp.syntax == "proto2"
40+
val proto3 = fdp.syntax == "proto3"
3841
}
3942

4043
fun respectJavaPackage(params: Map<String, String>) =

protokt-codegen/src/main/kotlin/com/toasttab/protokt/codegen/protoc/Types.kt

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -142,7 +142,6 @@ class OneofOptions(
142142
class FileDesc(
143143
val name: String,
144144
val packageName: String,
145-
val version: Int,
146145
val options: FileOptions,
147146
val context: ProtocolContext,
148147
val sourceCodeInfo: DescriptorProtos.SourceCodeInfo

protokt-codegen/src/main/resources/dsl.stg

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -71,7 +71,7 @@ dslValue(p) ::= <%
7171
emptyMap()
7272
<elseif (p.repeated)>
7373
emptyList()
74-
<elseif (isMessage.(p.fieldType) || p.wrapped)>
74+
<elseif (isMessage.(p.fieldType) || p.wrapped || p.nullable)>
7575
null
7676
<else>
7777
<p.defaultValue>

protokt-codegen/src/main/resources/renderers.stg

Lines changed: 17 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -111,7 +111,7 @@ deserializeType(p) ::= <%
111111
%>
112112

113113
deserializeValue(p) ::= <%
114-
<if (p.repeated || isMessage.(p.fieldType) || p.wrapped)>
114+
<if (p.repeated || isMessage.(p.fieldType) || p.wrapped || p.nullable)>
115115
null
116116
<else>
117117
<p.defaultValue>
@@ -159,19 +159,29 @@ defaultValue(field, type, name) ::= <%
159159
emptyList()
160160
<elseif (isGeneratedType.(type))>
161161
<name><kotlinDefaultValues.(type)>
162+
<elseif (field.nullable)>
163+
null
162164
<else>
163165
<kotlinDefaultValues.(type)>
164166
<endif>
165167
%>
166168

167169
nonDefaultValue(field, name) ::= <%
168-
<if (field.repeated)>(<field.fieldName>.isNotEmpty())<\ >
169-
<elseif (isBytes.(field.type) || isString.(field.type))>(<name>.isNotEmpty())<\ >
170-
<elseif (isMessage.(field.type))>(<field.fieldName> != null)<\ >
171-
<elseif (isEnum.(field.type))>(<name>.value != 0)<\ >
170+
<if ((isMessage.(field.type) && !field.repeated) || field.optional)>
171+
(<field.fieldName> != null)<\ >
172+
<elseif (field.repeated)>
173+
(<field.fieldName>.isNotEmpty())<\ >
174+
<elseif (isBytes.(field.type) || isString.(field.type))>
175+
(<proto2Check()><name>.isNotEmpty())<\ >
176+
<elseif (isMessage.(field.type))>
177+
(<field.fieldName> != null)<\ >
178+
<elseif (isEnum.(field.type))>
179+
(<name>.value != 0)<\ >
172180
<elseif (field.type.scalar)>
173-
<if (isBool.(field.type))>(<name>)<\ >
174-
<else>(<name> != <kotlinDefaultValues.(field.type)>)<\ >
181+
<if (isBool.(field.type))>
182+
(<name>)<\ >
183+
<else>
184+
(<name> != <kotlinDefaultValues.(field.type)>)<\ >
175185
<endif>
176186
<endif>
177187
%>

testing/options/src/test/kotlin/com/toasttab/protokt/testing/options/NonNullableTest.kt

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -25,14 +25,14 @@ class NonNullableTest {
2525
fun `test declared nullability`() {
2626
assertThat(
2727
NonNullModel::class.declaredMemberProperties
28-
.first { it.name == "nonNullStringValue" }
28+
.single { it.name == "nonNullStringValue" }
2929
.returnType
3030
.isMarkedNullable
3131
).isFalse()
3232

3333
assertThat(
3434
NonNullModel::class.declaredMemberProperties
35-
.first { it.name == "nonNullOneof" }
35+
.single { it.name == "nonNullOneof" }
3636
.returnType
3737
.isMarkedNullable
3838
).isFalse()
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
/*
2+
* Copyright (c) 2020 Toast Inc.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
* http://www.apache.org/licenses/LICENSE-2.0
8+
*
9+
* Unless required by applicable law or agreed to in writing, software
10+
* distributed under the License is distributed on an "AS IS" BASIS,
11+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
* See the License for the specific language governing permissions and
13+
* limitations under the License.
14+
*/
15+
16+
syntax = "proto3";
17+
18+
package com.toasttab.protokt.testing.rt;
19+
20+
message TestProto3Optional {
21+
optional int32 optional_int32 = 1;
22+
optional string optional_string = 2;
23+
24+
// doesn't do anything, but must be supported
25+
optional Foo optional_foo = 3;
26+
}
27+
28+
message Foo {}
Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,70 @@
1+
/*
2+
* Copyright (c) 2020 Toast Inc.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
* http://www.apache.org/licenses/LICENSE-2.0
8+
*
9+
* Unless required by applicable law or agreed to in writing, software
10+
* distributed under the License is distributed on an "AS IS" BASIS,
11+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
* See the License for the specific language governing permissions and
13+
* limitations under the License.
14+
*/
15+
16+
package com.toasttab.protokt.testing.rt
17+
18+
import com.google.common.truth.Truth.assertThat
19+
import kotlin.reflect.full.declaredMemberProperties
20+
import org.junit.jupiter.api.Test
21+
22+
class Proto3OptionalTest {
23+
@Test
24+
fun `optional scalar fields should be nullable`() {
25+
assertThat(
26+
TestProto3Optional::class.declaredMemberProperties
27+
.single { it.name == "optionalInt32" }
28+
.returnType
29+
.isMarkedNullable
30+
).isTrue()
31+
32+
assertThat(
33+
TestProto3Optional::class.declaredMemberProperties
34+
.single { it.name == "optionalString" }
35+
.returnType
36+
.isMarkedNullable
37+
).isTrue()
38+
39+
assertThat(
40+
TestProto3Optional::class.declaredMemberProperties
41+
.single { it.name == "optionalFoo" }
42+
.returnType
43+
.isMarkedNullable
44+
).isTrue()
45+
}
46+
47+
@Test
48+
fun `optional fields serialize otherwise default values`() {
49+
val serialized =
50+
TestProto3Optional {
51+
optionalInt32 = 0
52+
optionalString = ""
53+
}.serialize()
54+
55+
val deserialized = TestProto3Optional.deserialize(serialized)
56+
57+
assertThat(deserialized.optionalInt32).isEqualTo(0)
58+
assertThat(deserialized.optionalString).isEqualTo("")
59+
}
60+
61+
@Test
62+
fun `optional fields don't serialize default values for nulls`() {
63+
val serialized = TestProto3Optional { }.serialize()
64+
65+
val deserialized = TestProto3Optional.deserialize(serialized)
66+
67+
assertThat(deserialized.optionalInt32).isNull()
68+
assertThat(deserialized.optionalString).isNull()
69+
}
70+
}

0 commit comments

Comments
 (0)