Skip to content

Commit 81ec2e1

Browse files
committed
Add option to remove enum prefix
Signed-off-by: Seonghyeon Cho <[email protected]>
1 parent 6608a18 commit 81ec2e1

File tree

9 files changed

+252
-8
lines changed

9 files changed

+252
-8
lines changed

README.md

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -155,11 +155,12 @@ protobuf {
155155

156156
### Available Options
157157

158-
| Option | Description | Default |
159-
|--------------------|------------------------------------------------------------------------------------------------------------|---------|
160-
| `package_prefix` | Prefix for the generated package names. Appended to the start of each class | `""` |
161-
| `useCamelCase` | Whether to use the original `snake_case` for proto fields or `camelCase`. Can be either `true` or `false`. | `true` |
162-
| `generateServices` | Whether to generate abstract gRPC stubs or not. Can be either `true` or `false`. | `true` |
158+
| Option | Description | Default |
159+
|----------------------|-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|---------|
160+
| `package_prefix` | Prefix for the generated package names. Appended to the start of each class | `""` |
161+
| `useCamelCase` | Whether to use the original `snake_case` for proto fields or `camelCase`. Can be either `true` or `false`. | `true` |
162+
| `generateServices` | Whether to generate abstract gRPC stubs or not. Can be either `true` or `false`. | `true` |
163+
| `remove_enum_prefix` | Strips the enum name prefix from enum values (e.g., `STATE_ACTIVE` becomes `ACTIVE` for enum `State`). Useful when proto enums follow the `ENUM_NAME_VALUE` naming convention. | `false` |
163164

164165

165166
## Roadmap

app/src/main/kotlin/dogacel/kotlinx/protobuf/gen/CodeGenerator.kt

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ import com.squareup.kotlinpoet.TypeName
1313
import com.squareup.kotlinpoet.TypeSpec
1414
import com.squareup.kotlinpoet.asTypeName
1515
import dogacel.kotlinx.protobuf.gen.DefaultValues.defaultValueOf
16+
import dogacel.kotlinx.protobuf.gen.Utils.removeEnumPrefixFromValue
1617
import dogacel.kotlinx.protobuf.gen.Utils.toFirstLower
1718
import dogacel.kotlinx.protobuf.gen.Utils.toLowerCamelCaseIf
1819
import kotlinx.serialization.Serializable
@@ -232,7 +233,7 @@ class CodeGenerator {
232233
val fieldTypeName = TypeNames.typeNameOf(fieldDescriptor, typeLinks)
233234
val fieldName = fieldDescriptor.name.toLowerCamelCaseIf(options.useCamelCase)
234235

235-
val defaultValue = defaultValueOf(fieldDescriptor, typeLinks)
236+
val defaultValue = defaultValueOf(fieldDescriptor, typeLinks, options)
236237

237238
return ParameterSpec.builder(fieldName, fieldTypeName)
238239
.addAnnotations(Annotations.annotationsOf(fieldDescriptor))
@@ -336,8 +337,14 @@ class CodeGenerator {
336337
.addAnnotation(Serializable::class)
337338

338339
enumDescriptor.values.forEach { valueDescriptor ->
340+
val enumValueName =
341+
if (options.removeEnumPrefix) {
342+
removeEnumPrefixFromValue(enumDescriptor.name, valueDescriptor.name)
343+
} else {
344+
valueDescriptor.name
345+
}
339346
typeSpec.addEnumConstant(
340-
valueDescriptor.name,
347+
enumValueName,
341348
TypeSpec.anonymousClassBuilder()
342349
.addAnnotations(Annotations.annotationsOf(valueDescriptor))
343350
.build(),

app/src/main/kotlin/dogacel/kotlinx/protobuf/gen/CodeGeneratorOptions.kt

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ data class CodeGeneratorOptions(
2525
val generateGrpcServices: Boolean = true,
2626
val generateGrpcMethodsSuspend: Boolean = true,
2727
val wellKnownTypes: WellKnownTypes = NoWellKnownTypes,
28+
val removeEnumPrefix: Boolean = false,
2829
) {
2930
companion object {
3031
fun parse(parameter: String): CodeGeneratorOptions {
@@ -43,6 +44,7 @@ data class CodeGeneratorOptions(
4344
flags.contains("use_well_known_types").let {
4445
if (it) DefaultWellKnownTypes else NoWellKnownTypes
4546
},
47+
removeEnumPrefix = flags.contains("remove_enum_prefix"),
4648
)
4749
}
4850
}

app/src/main/kotlin/dogacel/kotlinx/protobuf/gen/DefaultValues.kt

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ package dogacel.kotlinx.protobuf.gen
33
import com.google.protobuf.Descriptors
44
import com.squareup.kotlinpoet.CodeBlock
55
import com.squareup.kotlinpoet.TypeName
6+
import dogacel.kotlinx.protobuf.gen.Utils.removeEnumPrefixFromValue
67

78
object DefaultValues {
89
/**
@@ -11,11 +12,13 @@ object DefaultValues {
1112
* @param fieldDescriptor a [Descriptors.FieldDescriptor] to get default value for.
1213
* @param typeNames a map of [Descriptors.GenericDescriptor] to [TypeName] to get type names which is used
1314
* to reference other types using full name in code.
15+
* @param options optional [CodeGeneratorOptions] to customize the generated default value.
1416
* @return default value of the given type
1517
*/
1618
fun defaultValueOf(
1719
fieldDescriptor: Descriptors.FieldDescriptor,
1820
typeNames: Map<Descriptors.GenericDescriptor, TypeName> = mapOf(),
21+
options: CodeGeneratorOptions? = null,
1922
): Any? {
2023
if (fieldDescriptor.realContainingOneof != null) {
2124
return null
@@ -56,7 +59,16 @@ object DefaultValues {
5659
typeNames[fieldDescriptor.enumType]
5760
?: throw IllegalStateException("Enum type not found: ${fieldDescriptor.enumType.fullName}")
5861

59-
return CodeBlock.of("%L.%L", typeName, fieldDescriptor.defaultValue)
62+
val enumValueName =
63+
if (options?.removeEnumPrefix == true) {
64+
val enumDescriptor = fieldDescriptor.enumType
65+
val defaultValueDescriptor = fieldDescriptor.defaultValue as Descriptors.EnumValueDescriptor
66+
removeEnumPrefixFromValue(enumDescriptor.name, defaultValueDescriptor.name)
67+
} else {
68+
fieldDescriptor.defaultValue
69+
}
70+
71+
return CodeBlock.of("%L.%L", typeName, enumValueName)
6072
}
6173

6274
Descriptors.FieldDescriptor.Type.MESSAGE -> null

app/src/main/kotlin/dogacel/kotlinx/protobuf/gen/Utils.kt

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,4 +41,50 @@ object Utils {
4141
}
4242
return this[0].lowercaseChar() + this.substring(1)
4343
}
44+
45+
/**
46+
* Convert the string from PascalCase/camelCase to SCREAMING_SNAKE_CASE.
47+
*
48+
* Example:
49+
* - `ForeignEnum` -> `FOREIGN_ENUM`
50+
* - `State` -> `STATE`
51+
* - `MyEnumValue` -> `MY_ENUM_VALUE`
52+
*
53+
* @return The string in SCREAMING_SNAKE_CASE.
54+
*/
55+
fun String.toScreamingSnakeCase(): String {
56+
if (isEmpty()) return this
57+
val result = StringBuilder()
58+
for ((index, char) in withIndex()) {
59+
if (char.isUpperCase() && index > 0 && this[index - 1].isLowerCase()) {
60+
result.append('_')
61+
}
62+
result.append(char.uppercaseChar())
63+
}
64+
return result.toString()
65+
}
66+
67+
/**
68+
* Remove the enum name prefix from an enum value name if it matches the convention.
69+
*
70+
* Example:
71+
* - `removeEnumPrefixFromValue("State", "STATE_ACTIVE")` -> `ACTIVE`
72+
* - `removeEnumPrefixFromValue("State", "STATE_UNKNOWN")` -> `UNKNOWN`
73+
* - `removeEnumPrefixFromValue("State", "INVALID")` -> `INVALID` (no match)
74+
*
75+
* @param enumName The name of the enum (e.g., "State", "ForeignEnum")
76+
* @param valueName The name of the enum value (e.g., "STATE_ACTIVE", "FOREIGN_ENUM_FOO")
77+
* @return The value name with the prefix removed if it matches, otherwise the original value name.
78+
*/
79+
fun removeEnumPrefixFromValue(
80+
enumName: String,
81+
valueName: String,
82+
): String {
83+
val prefix = enumName.toScreamingSnakeCase() + "_"
84+
return if (valueName.startsWith(prefix) && valueName.length > prefix.length) {
85+
valueName.removePrefix(prefix)
86+
} else {
87+
valueName
88+
}
89+
}
4490
}
Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
package dogacel.kotlinx.protobuf.gen
2+
3+
import kotlin.test.Test
4+
import kotlin.test.assertEquals
5+
import kotlin.test.assertFalse
6+
import kotlin.test.assertTrue
7+
8+
class CodeGeneratorOptionsTest {
9+
@Test
10+
fun parse_removeEnumPrefix_enabled() {
11+
val options = CodeGeneratorOptions.parse("remove_enum_prefix")
12+
assertTrue(options.removeEnumPrefix)
13+
}
14+
15+
@Test
16+
fun parse_removeEnumPrefix_disabled_byDefault() {
17+
val options = CodeGeneratorOptions.parse("")
18+
assertFalse(options.removeEnumPrefix)
19+
}
20+
21+
@Test
22+
fun parse_removeEnumPrefix_withOtherFlags() {
23+
val options = CodeGeneratorOptions.parse("remove_enum_prefix,use_snake_case")
24+
assertTrue(options.removeEnumPrefix)
25+
assertFalse(options.useCamelCase)
26+
}
27+
28+
@Test
29+
fun parse_removeEnumPrefix_withPackagePrefix() {
30+
val options = CodeGeneratorOptions.parse("package_prefix=custom.pkg,remove_enum_prefix")
31+
assertTrue(options.removeEnumPrefix)
32+
assertEquals("custom.pkg", options.packagePrefix)
33+
}
34+
35+
@Test
36+
fun defaultOptions_removeEnumPrefix_isFalse() {
37+
val options = CodeGeneratorOptions()
38+
assertFalse(options.removeEnumPrefix)
39+
}
40+
}
Lines changed: 83 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,83 @@
1+
package dogacel.kotlinx.protobuf.gen
2+
3+
import dogacel.kotlinx.protobuf.gen.Utils.removeEnumPrefixFromValue
4+
import dogacel.kotlinx.protobuf.gen.Utils.toScreamingSnakeCase
5+
import kotlin.test.Test
6+
import kotlin.test.assertEquals
7+
8+
class UtilsTest {
9+
@Test
10+
fun toScreamingSnakeCase_simpleCase() {
11+
assertEquals("STATE", "State".toScreamingSnakeCase())
12+
}
13+
14+
@Test
15+
fun toScreamingSnakeCase_multiWord() {
16+
assertEquals("FOREIGN_ENUM", "ForeignEnum".toScreamingSnakeCase())
17+
}
18+
19+
@Test
20+
fun toScreamingSnakeCase_multipleUpperCase() {
21+
assertEquals("MY_ENUM_VALUE", "MyEnumValue".toScreamingSnakeCase())
22+
}
23+
24+
@Test
25+
fun toScreamingSnakeCase_alreadyUpperCase() {
26+
assertEquals("STATE", "STATE".toScreamingSnakeCase())
27+
}
28+
29+
@Test
30+
fun toScreamingSnakeCase_emptyString() {
31+
assertEquals("", "".toScreamingSnakeCase())
32+
}
33+
34+
@Test
35+
fun toScreamingSnakeCase_singleChar() {
36+
assertEquals("A", "a".toScreamingSnakeCase())
37+
assertEquals("A", "A".toScreamingSnakeCase())
38+
}
39+
40+
@Test
41+
fun toScreamingSnakeCase_camelCase() {
42+
assertEquals("CAMEL_CASE", "camelCase".toScreamingSnakeCase())
43+
}
44+
45+
@Test
46+
fun removeEnumPrefixFromValue_matchingPrefix() {
47+
assertEquals("ACTIVE", removeEnumPrefixFromValue("State", "STATE_ACTIVE"))
48+
assertEquals("UNKNOWN", removeEnumPrefixFromValue("State", "STATE_UNKNOWN"))
49+
}
50+
51+
@Test
52+
fun removeEnumPrefixFromValue_multiWordEnumName() {
53+
assertEquals("FOO", removeEnumPrefixFromValue("ForeignEnum", "FOREIGN_ENUM_FOO"))
54+
assertEquals("BAR_BAZ", removeEnumPrefixFromValue("ForeignEnum", "FOREIGN_ENUM_BAR_BAZ"))
55+
}
56+
57+
@Test
58+
fun removeEnumPrefixFromValue_noMatch() {
59+
assertEquals("INVALID", removeEnumPrefixFromValue("State", "INVALID"))
60+
assertEquals("OTHER_VALUE", removeEnumPrefixFromValue("State", "OTHER_VALUE"))
61+
}
62+
63+
@Test
64+
fun removeEnumPrefixFromValue_partialMatch() {
65+
assertEquals("ALIAS_FOO", removeEnumPrefixFromValue("AliasedEnum", "ALIAS_FOO"))
66+
}
67+
68+
@Test
69+
fun removeEnumPrefixFromValue_prefixOnly() {
70+
assertEquals("STATE_", removeEnumPrefixFromValue("State", "STATE_"))
71+
}
72+
73+
@Test
74+
fun removeEnumPrefixFromValue_exactPrefix() {
75+
assertEquals("STATE", removeEnumPrefixFromValue("State", "STATE"))
76+
}
77+
78+
@Test
79+
fun removeEnumPrefixFromValue_emptyEnumName() {
80+
assertEquals("VALUE", removeEnumPrefixFromValue("", "_VALUE"))
81+
assertEquals("VALUE", removeEnumPrefixFromValue("", "VALUE"))
82+
}
83+
}
Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
package testgen.enums_prefix.proto3
2+
3+
import kotlinx.serialization.Serializable
4+
import kotlinx.serialization.protobuf.ProtoNumber
5+
6+
@Serializable
7+
public data class StatusMessage(
8+
@ProtoNumber(number = 1)
9+
public val state: State = testgen.enums_prefix.proto3.State.STATE_UNKNOWN,
10+
@ProtoNumber(number = 2)
11+
public val foreignEnum:
12+
ForeignEnum = testgen.enums_prefix.proto3.ForeignEnum.FOREIGN_ENUM_UNSPECIFIED,
13+
)
14+
15+
@Serializable
16+
public enum class State {
17+
@ProtoNumber(number = 0)
18+
STATE_UNKNOWN,
19+
@ProtoNumber(number = 1)
20+
STATE_ACTIVE,
21+
@ProtoNumber(number = 2)
22+
STATE_INACTIVE,
23+
}
24+
25+
@Serializable
26+
public enum class ForeignEnum {
27+
@ProtoNumber(number = 0)
28+
FOREIGN_ENUM_UNSPECIFIED,
29+
@ProtoNumber(number = 1)
30+
FOREIGN_ENUM_FOO,
31+
@ProtoNumber(number = 2)
32+
FOREIGN_ENUM_BAR,
33+
}
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
syntax = "proto3";
2+
3+
package enums_prefix.proto3;
4+
5+
enum State {
6+
STATE_UNKNOWN = 0;
7+
STATE_ACTIVE = 1;
8+
STATE_INACTIVE = 2;
9+
}
10+
11+
enum ForeignEnum {
12+
FOREIGN_ENUM_UNSPECIFIED = 0;
13+
FOREIGN_ENUM_FOO = 1;
14+
FOREIGN_ENUM_BAR = 2;
15+
}
16+
17+
message StatusMessage {
18+
State state = 1;
19+
ForeignEnum foreign_enum = 2;
20+
}

0 commit comments

Comments
 (0)