Skip to content

Commit 158b59d

Browse files
authored
Feature: Consider extension fields for isInitialized computation (#129)
* Add naming strategy. Set default naming strategy to Kotlin idiomatic. Extend name conflict resolution to classes and files. * Change conflict resolution strategy for files to numbers instead of underscores. Add tests * Add LEGACY naming strategy. Fix tests. * Fix behavior of LEGACY naming strategy. * Fix source generation. Fix tests. includeWellKnownTypes may only be used with KOTLIN_IDIOMATIC naming strategy. * Use KOTLIN_IDIOMATIC naming strategy in tests * Fix tests * Also consider extensions for isInitialized. * Fix tests.
1 parent 9427cdd commit 158b59d

File tree

4 files changed

+130
-6
lines changed

4 files changed

+130
-6
lines changed

kmp-grpc-internal-test/src/commonMain/proto/proto2/proto2-required-fields.proto

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,3 +20,15 @@ message Proto2MessageWithRequiredFields {
2020
string field6 = 6;
2121
}
2222
}
23+
24+
message Proto2MessageWithRequiredExtension {
25+
extensions 1 to max;
26+
}
27+
28+
extend Proto2MessageWithRequiredExtension {
29+
optional string extension1 = 1;
30+
31+
optional Proto2MessageWithMixedFields extensionRequiredMsg = 2;
32+
33+
repeated Proto2MessageWithMixedFields extensionRepeatedMsg = 3;
34+
}

kmp-grpc-internal-test/src/commonTest/kotlin/io/github/timortel/kotlin_multiplatform_grpc_plugin/test/model/IsInitializedTest.kt

Lines changed: 66 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,9 @@
11
package io.github.timortel.kotlin_multiplatform_grpc_plugin.test.model
22

3+
import io.github.timortel.kmpgrpc.core.message.extensions.buildExtensions
34
import io.github.timortel.kmpgrpc.test.proto2.Proto2RequiredFields
5+
import io.github.timortel.kmpgrpc.test.proto2.Proto2RequiredFields.Proto2MessageWithMixedFields
6+
import io.github.timortel.kmpgrpc.test.proto2.Proto2RequiredFields.Proto2MessageWithRequiredExtension
47
import io.github.timortel.kmpgrpc.test.proto2.Proto2RequiredFields.Proto2MessageWithRequiredFields
58
import kotlin.test.Test
69
import kotlin.test.assertFalse
@@ -25,7 +28,7 @@ class IsInitializedTest {
2528
@Test
2629
fun testUninitializedNestedMessage() {
2730
// field2 is set, but the nested message itself is missing its own required field1
28-
val incompleteNested = Proto2RequiredFields.Proto2MessageWithMixedFields.createPartial(field1 = null)
31+
val incompleteNested = Proto2MessageWithMixedFields.createPartial(field1 = null)
2932
val msg = Proto2MessageWithRequiredFields.createPartial(
3033
field1 = "valid",
3134
field2 = incompleteNested
@@ -41,7 +44,10 @@ class IsInitializedTest {
4144
field3List = listOf(incomplete)
4245
)
4346

44-
assertFalse(msg.isInitialized, "Message should be uninitialized if any element in a repeated field is uninitialized")
47+
assertFalse(
48+
msg.isInitialized,
49+
"Message should be uninitialized if any element in a repeated field is uninitialized"
50+
)
4551
}
4652

4753
@Test
@@ -57,7 +63,7 @@ class IsInitializedTest {
5763
@Test
5864
fun testOneOfInitialization() {
5965
// x.field5 is a message type. If that message is incomplete, the parent is incomplete.
60-
val incompleteMixed = Proto2RequiredFields.Proto2MessageWithMixedFields.createPartial(field1 = null)
66+
val incompleteMixed = Proto2MessageWithMixedFields.createPartial(field1 = null)
6167
val msg = Proto2MessageWithRequiredFields(
6268
x = Proto2MessageWithRequiredFields.X.Field5(incompleteMixed)
6369
)
@@ -70,4 +76,61 @@ class IsInitializedTest {
7076
)
7177
assertTrue(msg2.isInitialized, "Message should be initialized if OneOf contains a valid string")
7278
}
79+
80+
@Test
81+
fun testRequiredMessageExtensionInitialization() {
82+
// 1. Missing both required extensions
83+
val emptyMsg = Proto2MessageWithRequiredExtension.createPartial()
84+
assertFalse(emptyMsg.isInitialized, "Should be uninitialized: missing extension1 and extensionRequiredMsg")
85+
86+
// 2. extension1 is present, but extensionRequiredMsg is missing
87+
val partialExt1 = buildExtensions {
88+
set(Proto2RequiredFields.extension1, "valid")
89+
}
90+
val msgOnlyExt1 = Proto2MessageWithRequiredExtension.createPartial(extensions = partialExt1)
91+
assertFalse(msgOnlyExt1.isInitialized, "Should be uninitialized: missing required message extension")
92+
93+
// 3. Both present, but the required message extension is itself uninitialized
94+
val incompleteNested = Proto2MessageWithMixedFields.createPartial(field1 = null)
95+
val partialExt2 = buildExtensions {
96+
set(Proto2RequiredFields.extension1, "valid")
97+
set(Proto2RequiredFields.extensionRequiredMsg, incompleteNested)
98+
}
99+
val msgIncompleteMsg = Proto2MessageWithRequiredExtension.createPartial(extensions = partialExt2)
100+
assertFalse(
101+
msgIncompleteMsg.isInitialized,
102+
"Should be uninitialized: required message extension is missing field1"
103+
)
104+
105+
// 4. Fully initialized
106+
val completeExt = buildExtensions {
107+
set(Proto2RequiredFields.extension1, "valid")
108+
set(Proto2RequiredFields.extensionRequiredMsg, Proto2MessageWithMixedFields(field1 = "valid"))
109+
}
110+
val validMsg = Proto2MessageWithRequiredExtension(extensions = completeExt)
111+
assertTrue(
112+
validMsg.isInitialized,
113+
"Should be initialized: all required extensions and their fields are present"
114+
)
115+
}
116+
117+
@Test
118+
fun testRepeatedMessageExtensionInitialization() {
119+
val validNested = Proto2MessageWithMixedFields(field1 = "ok")
120+
val incompleteNested = Proto2MessageWithMixedFields.createPartial(field1 = null)
121+
122+
// Base valid extensions so the parent's 'required' constraints are met
123+
val baseExtensions = buildExtensions {
124+
set(Proto2RequiredFields.extension1, "valid")
125+
set(Proto2RequiredFields.extensionRequiredMsg, validNested)
126+
set(Proto2RequiredFields.extensionRepeatedMsgList, listOf(validNested, incompleteNested))
127+
}
128+
129+
val msg = Proto2MessageWithRequiredExtension(extensions = baseExtensions)
130+
131+
assertFalse(
132+
msg.isInitialized,
133+
"Should be uninitialized: one element in the repeated message extension is uninitialized"
134+
)
135+
}
73136
}

kmp-grpc-plugin/src/main/java/io/github/timortel/kmpgrpc/plugin/sourcegeneration/generators/protofile/message/extensions/IsInitializedFieldExtension.kt

Lines changed: 51 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ package io.github.timortel.kmpgrpc.plugin.sourcegeneration.generators.protofile.
22

33
import com.squareup.kotlinpoet.CodeBlock
44
import com.squareup.kotlinpoet.KModifier
5+
import com.squareup.kotlinpoet.MemberName
56
import com.squareup.kotlinpoet.TypeSpec
67
import io.github.timortel.kmpgrpc.plugin.sourcegeneration.SourceTarget
78
import io.github.timortel.kmpgrpc.plugin.sourcegeneration.constants.Const
@@ -34,7 +35,13 @@ object IsInitializedFieldExtension : MessageWriterExtension {
3435

3536
val subMessages = subMessageFields + subMessageMapFields + oneOfs
3637

37-
if (requiredFields.isEmpty() && subMessages.isEmpty()) {
38+
val consideredExtensionFields = message.extensionsInProject.flatMap { extensions ->
39+
extensions.fields.filter { field ->
40+
field.cardinality.isLegacyRequired || field.type.isMessage
41+
}
42+
}
43+
44+
if (requiredFields.isEmpty() && subMessages.isEmpty() && consideredExtensionFields.isEmpty()) {
3845
add("true")
3946
} else {
4047
val separator = " && "
@@ -51,6 +58,7 @@ object IsInitializedFieldExtension : MessageWriterExtension {
5158
Const.Message.isInitializedProperty.name
5259
)
5360
}
61+
5462
ProtoFieldCardinality.Repeated -> {
5563
add(
5664
"%N.all { it.%N }",
@@ -77,7 +85,48 @@ object IsInitializedFieldExtension : MessageWriterExtension {
7785
)
7886
}
7987

80-
val impl = listOf(requiredFieldsBool, subMessageFieldsBool, subMessageOneOfFieldsBool, subMessageMapFieldsBool).joinCodeBlocks(separator)
88+
val requiredExtensionFieldsBool =
89+
consideredExtensionFields.joinToCodeBlock(separator) { field ->
90+
val extensionMember = MemberName(field.file.className, field.codeName)
91+
92+
when (field.cardinality) {
93+
is ProtoFieldCardinality.Singular -> {
94+
if (field.type.isMessage) {
95+
add(
96+
"%N[%M]?.%N == true",
97+
Const.Message.Constructor.MessageExtensions.name,
98+
extensionMember,
99+
Const.Message.isInitializedProperty.name
100+
)
101+
} else {
102+
add(
103+
"%N[%M] != null",
104+
Const.Message.Constructor.MessageExtensions.name,
105+
extensionMember
106+
)
107+
}
108+
}
109+
110+
ProtoFieldCardinality.Repeated -> {
111+
if (field.type.isMessage) {
112+
add(
113+
"%N[%M].all { it.%N }",
114+
Const.Message.Constructor.MessageExtensions.name,
115+
extensionMember,
116+
Const.Message.isInitializedProperty.name
117+
)
118+
}
119+
}
120+
}
121+
}
122+
123+
val impl = listOf(
124+
requiredFieldsBool,
125+
subMessageFieldsBool,
126+
subMessageOneOfFieldsBool,
127+
subMessageMapFieldsBool,
128+
requiredExtensionFieldsBool
129+
).joinCodeBlocks(separator)
81130

82131
add(impl)
83132
}

readme.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -367,7 +367,7 @@ You can construct a message of type `MyMessage` like this:
367367
val msg = Sample.MyMessage(
368368
regularField = "val1",
369369
extensions = buildExtensions {
370-
set[Sample.myExtension] = "val2"
370+
set(Sample.myExtension, "val2")
371371
}
372372
)
373373
```

0 commit comments

Comments
 (0)