├── gradle.properties ├── gradle └── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── settings.gradle.kts ├── .gitignore ├── core ├── src │ └── main │ │ └── kotlin │ │ └── io │ │ └── github │ │ └── mscheong01 │ │ └── krotodc │ │ ├── KrotoDCConverter.kt │ │ ├── KrotoDC.kt │ │ └── Extensions.kt └── build.gradle.kts ├── generator ├── src │ ├── test │ │ ├── proto │ │ │ ├── very_long_message.proto │ │ │ ├── file_with_java_outer_classname.proto │ │ │ ├── default_values_test.proto │ │ │ ├── import_from_other_file_test.proto │ │ │ ├── keyword.proto │ │ │ ├── test.proto │ │ │ ├── rpc.proto │ │ │ └── well_known_types.proto │ │ └── kotlin │ │ │ └── io │ │ │ └── github │ │ │ └── mscheong01 │ │ │ └── krotodc │ │ │ ├── JsonConverterTest.kt │ │ │ ├── RpcTest.kt │ │ │ ├── KeywordEscapeTest.kt │ │ │ └── ConversionTest.kt │ └── main │ │ └── kotlin │ │ └── io │ │ └── github │ │ └── mscheong01 │ │ └── krotodc │ │ ├── import │ │ ├── Import.kt │ │ ├── FunSpecsWithImports.kt │ │ ├── TypeSpecsWithImports.kt │ │ ├── StringWithImports.kt │ │ └── CodeWithImports.kt │ │ ├── util │ │ ├── Constants.kt │ │ ├── Capitalization.kt │ │ ├── Keyword.kt │ │ ├── KotlinPoetExtensions.kt │ │ ├── FieldName.kt │ │ └── DescriptorExtensions.kt │ │ ├── specgenerators │ │ ├── FunSpecGenerator.kt │ │ ├── TypeSpecGenerator.kt │ │ ├── FileSpecGenerator.kt │ │ ├── function │ │ │ ├── ToJsonFunctionGenerator.kt │ │ │ ├── FromJsonFunctionGenerator.kt │ │ │ ├── MessageToProtoFunctionGenerator.kt │ │ │ └── MessageToDataClassFunctionGenerator.kt │ │ ├── file │ │ │ ├── GrpcKrotoGenerator.kt │ │ │ ├── ConverterGenerator.kt │ │ │ └── DataClassGenerator.kt │ │ └── type │ │ │ ├── ClientStubGenerator.kt │ │ │ └── ServiceImplBaseGenerator.kt │ │ ├── MainExecutor.kt │ │ ├── template │ │ ├── TransformTemplateWithImports.kt │ │ └── TransformTemplate.kt │ │ ├── KrotoDCCodeGenerator.kt │ │ └── predefinedtypes │ │ └── HandledPreDefinedType.kt ├── build.gradle.kts └── README.md ├── example ├── src │ └── main │ │ ├── proto │ │ └── server.proto │ │ └── kotlin │ │ └── io │ │ └── github │ │ └── mscheong01 │ │ └── krotodc │ │ └── example │ │ ├── Server.kt │ │ ├── SimpleServiceImpl.kt │ │ └── Client.kt └── build.gradle.kts ├── .github └── workflows │ ├── test.yml │ ├── ktlint-check.yml │ ├── publish-snapshot.yml │ └── publish-release.yml ├── CONTRIBUTING.md ├── README.md ├── gradlew.bat ├── gradlew └── LICENSE /gradle.properties: -------------------------------------------------------------------------------- 1 | #Wed Apr 16 07:07:23 UTC 2025 2 | kotlin.code.style=official 3 | version=1.3.0-SNAPSHOT 4 | -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mscheong01/krotoDC/HEAD/gradle/wrapper/gradle-wrapper.jar -------------------------------------------------------------------------------- /settings.gradle.kts: -------------------------------------------------------------------------------- 1 | rootProject.name = "krotoDC" 2 | include("generator") 3 | include("core") 4 | include("example") 5 | -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | distributionBase=GRADLE_USER_HOME 2 | distributionPath=wrapper/dists 3 | distributionUrl=https\://services.gradle.org/distributions/gradle-9.2.0-bin.zip 4 | zipStoreBase=GRADLE_USER_HOME 5 | zipStorePath=wrapper/dists 6 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .classpath 2 | .gradle 3 | .project 4 | .settings 5 | eclipsebin 6 | local.properties 7 | 8 | bin 9 | gen 10 | build 11 | out 12 | lib 13 | reports 14 | 15 | .idea 16 | *.iml 17 | classes 18 | 19 | # Mkdocs files 20 | docs/1.x/* 21 | 22 | obj 23 | 24 | .DS_Store 25 | -------------------------------------------------------------------------------- /core/src/main/kotlin/io/github/mscheong01/krotodc/KrotoDCConverter.kt: -------------------------------------------------------------------------------- 1 | package io.github.mscheong01.krotodc 2 | 3 | import kotlin.reflect.KClass 4 | 5 | @Target(AnnotationTarget.FUNCTION) 6 | annotation class KrotoDCConverter( 7 | val from: KClass, 8 | val to: KClass 9 | ) 10 | -------------------------------------------------------------------------------- /core/src/main/kotlin/io/github/mscheong01/krotodc/KrotoDC.kt: -------------------------------------------------------------------------------- 1 | package io.github.mscheong01.krotodc 2 | 3 | import com.google.protobuf.GeneratedMessage 4 | import kotlin.reflect.KClass 5 | 6 | @Target(AnnotationTarget.CLASS) 7 | annotation class KrotoDC( 8 | val forProto: KClass 9 | ) 10 | -------------------------------------------------------------------------------- /generator/src/test/proto/very_long_message.proto: -------------------------------------------------------------------------------- 1 | syntax = "proto3"; 2 | 3 | option java_multiple_files = true; 4 | option java_package = "com.example.application.system.types"; 5 | option java_outer_classname = "VeryLongMessageNameProto"; 6 | 7 | package com.example.application.system.types; 8 | 9 | message VeryVeryLongTestProtocolBuffersMessageName { 10 | repeated string type = 1; 11 | } 12 | -------------------------------------------------------------------------------- /example/src/main/proto/server.proto: -------------------------------------------------------------------------------- 1 | syntax = "proto3"; 2 | 3 | option java_multiple_files = true; 4 | option java_package = "com.example.grpc"; 5 | option java_outer_classname = "ServiceProto"; 6 | 7 | package service; 8 | 9 | service SimpleService { 10 | rpc SayHello (HelloRequest) returns (HelloResponse); 11 | } 12 | 13 | message HelloRequest { 14 | string name = 1; 15 | } 16 | 17 | message HelloResponse { 18 | string greeting = 1; 19 | } 20 | -------------------------------------------------------------------------------- /.github/workflows/test.yml: -------------------------------------------------------------------------------- 1 | name: Test 2 | 3 | on: 4 | pull_request: 5 | workflow_dispatch: 6 | 7 | jobs: 8 | test: 9 | runs-on: ubuntu-latest 10 | 11 | steps: 12 | - name: Checkout 13 | uses: actions/checkout@v2 14 | - name: Setup AdoptOpenJDK 15 | uses: joschi/setup-jdk@v2 16 | with: 17 | java-version: '17' 18 | - name: Test with Gradle 19 | working-directory: ./ 20 | run: ./gradlew test 21 | -------------------------------------------------------------------------------- /.github/workflows/ktlint-check.yml: -------------------------------------------------------------------------------- 1 | name: Ktlint Check 2 | 3 | on: 4 | pull_request: 5 | workflow_dispatch: 6 | 7 | jobs: 8 | ktlint-check: 9 | runs-on: ubuntu-latest 10 | 11 | steps: 12 | - name: Checkout 13 | uses: actions/checkout@v2 14 | - name: Setup AdoptOpenJDK 15 | uses: joschi/setup-jdk@v2 16 | with: 17 | java-version: '17' 18 | - name: Ktlint Check with Gradle 19 | working-directory: ./ 20 | run: ./gradlew clean ktlintCheck 21 | -------------------------------------------------------------------------------- /generator/src/main/kotlin/io/github/mscheong01/krotodc/import/Import.kt: -------------------------------------------------------------------------------- 1 | // Copyright 2023 Minsoo Cheong 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 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 | package io.github.mscheong01.krotodc.import 15 | 16 | data class Import( 17 | val packageName: String, 18 | val simpleNames: List 19 | ) 20 | -------------------------------------------------------------------------------- /generator/src/main/kotlin/io/github/mscheong01/krotodc/util/Constants.kt: -------------------------------------------------------------------------------- 1 | // Copyright 2023 Minsoo Cheong 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 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 | package io.github.mscheong01.krotodc.util 15 | 16 | internal const val MAP_ENTRY_KEY_FIELD_NUMBER = 1 17 | internal const val MAP_ENTRY_VALUE_FIELD_NUMBER = 2 18 | internal const val GRPC_JAVA_CLASS_NAME_SUFFIX = "Grpc" 19 | -------------------------------------------------------------------------------- /generator/src/test/proto/file_with_java_outer_classname.proto: -------------------------------------------------------------------------------- 1 | // Copyright 2023 Minsoo Cheong 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 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 | syntax = "proto3"; 15 | 16 | 17 | package com.example.outerclassnametest; 18 | 19 | option java_package = "com.example.importtest"; 20 | 21 | option java_outer_classname = "OuterClassNameTestProto"; 22 | 23 | message SimpleMessage { 24 | string name = 1; 25 | } 26 | -------------------------------------------------------------------------------- /generator/src/main/kotlin/io/github/mscheong01/krotodc/util/Capitalization.kt: -------------------------------------------------------------------------------- 1 | // Copyright 2023 Minsoo Cheong 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 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 | package io.github.mscheong01.krotodc.util 15 | 16 | fun String.capitalize(): String { 17 | return this.replaceFirstChar { it.uppercase() } 18 | } 19 | fun String.decapitalize(): String { 20 | return this.replaceFirstChar { it.lowercase() } 21 | } 22 | -------------------------------------------------------------------------------- /generator/src/main/kotlin/io/github/mscheong01/krotodc/util/Keyword.kt: -------------------------------------------------------------------------------- 1 | // Copyright 2023 Minsoo Cheong 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 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 | package io.github.mscheong01.krotodc.util 15 | 16 | import com.squareup.kotlinpoet.CodeBlock 17 | 18 | // escape a string if necessary using Kotlinpoet API 19 | fun String.escapeIfNecessary(): String { 20 | return CodeBlock.of("%N", this).toString() 21 | } 22 | -------------------------------------------------------------------------------- /generator/src/main/kotlin/io/github/mscheong01/krotodc/specgenerators/FunSpecGenerator.kt: -------------------------------------------------------------------------------- 1 | // Copyright 2023 Minsoo Cheong 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 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 | package io.github.mscheong01.krotodc.specgenerators 15 | 16 | import io.github.mscheong01.krotodc.import.FunSpecsWithImports 17 | 18 | interface FunSpecGenerator { 19 | fun generate(descriptor: Descriptor): FunSpecsWithImports 20 | } 21 | -------------------------------------------------------------------------------- /generator/src/main/kotlin/io/github/mscheong01/krotodc/specgenerators/TypeSpecGenerator.kt: -------------------------------------------------------------------------------- 1 | // Copyright 2023 Minsoo Cheong 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 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 | package io.github.mscheong01.krotodc.specgenerators 15 | 16 | import io.github.mscheong01.krotodc.import.TypeSpecsWithImports 17 | 18 | interface TypeSpecGenerator { 19 | fun generate(descriptor: Descriptor): TypeSpecsWithImports 20 | } 21 | -------------------------------------------------------------------------------- /core/build.gradle.kts: -------------------------------------------------------------------------------- 1 | 2 | dependencies { 3 | // implementation(kotlin("stdlib")) 4 | // implementation("io.grpc:grpc-protobuf:${rootProject.ext["grpcJavaVersion"]}") 5 | // https://mvnrepository.com/artifact/com.google.protobuf/protobuf-java 6 | implementation("com.google.protobuf:protobuf-java:${rootProject.ext["protobufVersion"]}") 7 | implementation("io.grpc:grpc-stub:${rootProject.ext["grpcJavaVersion"]}") 8 | implementation("io.grpc:grpc-kotlin-stub:${rootProject.ext["grpcKotlinVersion"]}") 9 | implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:${rootProject.ext["coroutinesVersion"]}") 10 | } 11 | 12 | configure { 13 | coordinates( 14 | groupId = project.group.toString(), 15 | artifactId = "krotoDC-core", 16 | version = project.version.toString() 17 | ) 18 | 19 | pom { 20 | name.set("krotoDC core library") 21 | description.set("provides runtime support for krotoDC generated code") 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /generator/src/main/kotlin/io/github/mscheong01/krotodc/import/FunSpecsWithImports.kt: -------------------------------------------------------------------------------- 1 | // Copyright 2023 Minsoo Cheong 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 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 | package io.github.mscheong01.krotodc.import 15 | 16 | import com.squareup.kotlinpoet.FunSpec 17 | 18 | data class FunSpecsWithImports( 19 | val funSpecs: List, 20 | val imports: Set = setOf() 21 | ) { 22 | companion object { 23 | val EMPTY = FunSpecsWithImports(emptyList()) 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /generator/src/test/proto/default_values_test.proto: -------------------------------------------------------------------------------- 1 | // Copyright 2023 Minsoo Cheong 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 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 | syntax = "proto3"; 15 | 16 | 17 | package com.example.defaultvaluestest; 18 | 19 | message DefaultValueTest { 20 | int32 int_field = 1; 21 | repeated string string_list = 2; 22 | DefaultValueNestedMessage default_value_nested_message = 3; 23 | } 24 | 25 | message DefaultValueNestedMessage { 26 | double double_field = 1; 27 | bool boolean_field = 2; 28 | } -------------------------------------------------------------------------------- /generator/src/main/kotlin/io/github/mscheong01/krotodc/import/TypeSpecsWithImports.kt: -------------------------------------------------------------------------------- 1 | // Copyright 2023 Minsoo Cheong 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 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 | package io.github.mscheong01.krotodc.import 15 | 16 | import com.squareup.kotlinpoet.TypeSpec 17 | 18 | data class TypeSpecsWithImports( 19 | val typeSpecs: List, 20 | val imports: Set 21 | ) { 22 | companion object { 23 | val EMPTY = TypeSpecsWithImports(emptyList(), emptySet()) 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /example/src/main/kotlin/io/github/mscheong01/krotodc/example/Server.kt: -------------------------------------------------------------------------------- 1 | // Copyright 2023 Minsoo Cheong 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 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 | package io.github.mscheong01.krotodc.example 15 | 16 | import io.grpc.ServerBuilder 17 | 18 | fun main() { 19 | val server = ServerBuilder 20 | .forPort(8080) 21 | .addService(SimpleServiceImpl()) 22 | .build() 23 | 24 | server.start() 25 | println("Server started, listening on ${server.port}") 26 | 27 | server.awaitTermination() 28 | } 29 | -------------------------------------------------------------------------------- /generator/src/main/kotlin/io/github/mscheong01/krotodc/import/StringWithImports.kt: -------------------------------------------------------------------------------- 1 | // Copyright 2023 Minsoo Cheong 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 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 | package io.github.mscheong01.krotodc.import 15 | 16 | data class StringWithImports( 17 | val string: String, 18 | val imports: Set = setOf() 19 | ) { 20 | companion object { 21 | fun of(string: String, imports: Set = setOf()): StringWithImports { 22 | return StringWithImports(string, imports) 23 | } 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /generator/src/main/kotlin/io/github/mscheong01/krotodc/specgenerators/FileSpecGenerator.kt: -------------------------------------------------------------------------------- 1 | // Copyright 2023 Minsoo Cheong 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 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 | package io.github.mscheong01.krotodc.specgenerators 15 | 16 | import com.google.protobuf.Descriptors.FileDescriptor 17 | import com.squareup.kotlinpoet.FileSpec 18 | 19 | /** 20 | * SubGenerators are responsible for generating part of krotoDC's output. 21 | */ 22 | interface FileSpecGenerator { 23 | fun generate(fileDescriptor: FileDescriptor): List 24 | } 25 | -------------------------------------------------------------------------------- /example/src/main/kotlin/io/github/mscheong01/krotodc/example/SimpleServiceImpl.kt: -------------------------------------------------------------------------------- 1 | // Copyright 2023 Minsoo Cheong 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 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 | package io.github.mscheong01.krotodc.example 15 | 16 | import com.example.grpc.krotodc.HelloRequest 17 | import com.example.grpc.krotodc.HelloResponse 18 | import com.example.grpc.krotodc.SimpleServiceGrpcKroto 19 | 20 | class SimpleServiceImpl : SimpleServiceGrpcKroto.SimpleServiceCoroutineImplBase() { 21 | override suspend fun sayHello(request: HelloRequest): HelloResponse { 22 | return HelloResponse( 23 | greeting = "Hello, ${request.name}!" 24 | ) 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /.github/workflows/publish-snapshot.yml: -------------------------------------------------------------------------------- 1 | name: Publish Snapshot 2 | 3 | on: 4 | push: 5 | branches: [ main ] 6 | paths: 7 | - 'generator/src/main/kotlin/**' 8 | - 'core/src/main/kotlin/**' 9 | - 'build.gradle.kts' 10 | - 'generator/build.gradle.kts' 11 | - 'core/build.gradle.kts' 12 | workflow_dispatch: 13 | 14 | env: 15 | ORG_GRADLE_PROJECT_mavenCentralUsername: ${{ secrets.MAVEN_CENTRAL_USERNAME }} 16 | ORG_GRADLE_PROJECT_mavenCentralPassword: ${{ secrets.MAVEN_CENTRAL_PASSWORD }} 17 | 18 | jobs: 19 | deploy: 20 | runs-on: ubuntu-latest 21 | 22 | steps: 23 | - name: Checkout 24 | uses: actions/checkout@v3 25 | 26 | - name: Setup JDK 27 | uses: joschi/setup-jdk@v2 28 | with: 29 | java-version: '17' 30 | 31 | - name: Build and Test 32 | run: ./gradlew build test 33 | 34 | - name: Publish Snapshot to Maven Central 35 | run: ./gradlew publishToMavenCentral --no-configuration-cache 36 | 37 | - name: Publish Test Report 38 | uses: mikepenz/action-junit-report@v2 39 | if: always() 40 | with: 41 | report_paths: '**/build/test-results/test/TEST-*.xml' -------------------------------------------------------------------------------- /generator/src/main/kotlin/io/github/mscheong01/krotodc/import/CodeWithImports.kt: -------------------------------------------------------------------------------- 1 | // Copyright 2023 Minsoo Cheong 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 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 | package io.github.mscheong01.krotodc.import 15 | 16 | import com.squareup.kotlinpoet.CodeBlock 17 | 18 | data class CodeWithImports( 19 | val code: CodeBlock, 20 | val imports: Set 21 | ) { 22 | companion object { 23 | fun of( 24 | code: CodeBlock, 25 | imports: Set = setOf() 26 | ) = CodeWithImports(code, imports) 27 | 28 | fun of( 29 | code: String, 30 | imports: Set = setOf() 31 | ) = CodeWithImports(CodeBlock.of(code), imports) 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /example/src/main/kotlin/io/github/mscheong01/krotodc/example/Client.kt: -------------------------------------------------------------------------------- 1 | // Copyright 2023 Minsoo Cheong 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 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 | package io.github.mscheong01.krotodc.example 15 | 16 | import com.example.grpc.krotodc.HelloRequest 17 | import com.example.grpc.krotodc.SimpleServiceGrpcKroto 18 | import io.grpc.ManagedChannelBuilder 19 | import kotlinx.coroutines.runBlocking 20 | 21 | fun main(): Unit = runBlocking { 22 | val channel = ManagedChannelBuilder.forAddress("localhost", 8080).usePlaintext().build() 23 | val stub = SimpleServiceGrpcKroto.SimpleServiceCoroutineStub(channel) 24 | val response = stub.sayHello( 25 | HelloRequest(name = "KrotoDC") 26 | ) 27 | println(response.greeting) 28 | } 29 | -------------------------------------------------------------------------------- /generator/src/main/kotlin/io/github/mscheong01/krotodc/util/KotlinPoetExtensions.kt: -------------------------------------------------------------------------------- 1 | // Copyright 2023 Minsoo Cheong 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 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 | package io.github.mscheong01.krotodc.util 15 | 16 | import com.squareup.kotlinpoet.FileSpec 17 | import com.squareup.kotlinpoet.FunSpec 18 | import io.github.mscheong01.krotodc.import.Import 19 | 20 | fun FileSpec.Builder.addImport(import: Import) { 21 | this.addImport(import.packageName, import.simpleNames.joinToString(".")) 22 | } 23 | 24 | fun FileSpec.Builder.addAllImports(imports: Set) { 25 | imports.forEach { this.addImport(it) } 26 | } 27 | 28 | fun FunSpec.Builder.endControlFlowWithComma() { 29 | this.addCode("⇤") 30 | this.addStatement("},") 31 | } 32 | -------------------------------------------------------------------------------- /generator/src/test/proto/import_from_other_file_test.proto: -------------------------------------------------------------------------------- 1 | // Copyright 2023 Minsoo Cheong 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 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 | syntax = "proto3"; 15 | 16 | import "test.proto"; 17 | import "file_with_java_outer_classname.proto"; 18 | 19 | package com.example.importtest; 20 | 21 | option java_package = "io.github.mscheong01.importtest"; 22 | 23 | 24 | message ImportTestMessage { 25 | // import TopLevelMessage.NestedMessage 26 | com.example.test.TopLevelMessage.NestedMessage imported_nested_message = 1; 27 | // import Person 28 | com.example.test.Person imported_person = 2; 29 | // import SimpleMessage with outer classname 30 | com.example.outerclassnametest.SimpleMessage imported_simple_message = 3; 31 | } 32 | -------------------------------------------------------------------------------- /generator/src/main/kotlin/io/github/mscheong01/krotodc/MainExecutor.kt: -------------------------------------------------------------------------------- 1 | // Copyright 2023 Minsoo Cheong 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 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 | package io.github.mscheong01.krotodc 15 | 16 | import com.google.protobuf.compiler.PluginProtos.CodeGeneratorRequest 17 | import java.io.IOException 18 | 19 | object MainExecutor { 20 | @JvmStatic 21 | fun main(args: Array) { 22 | val request = try { 23 | CodeGeneratorRequest.parseFrom(System.`in`) 24 | } catch (e: Throwable) { 25 | throw IOException("Error occurred while parsing CodeGeneratorRequest for krotoDC", e) 26 | } 27 | try { 28 | KrotoDCCodeGenerator.generateCode(request) 29 | } catch (e: Throwable) { 30 | throw IOException("Error occurred while generating code through krotoDC", e) 31 | } 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /generator/src/main/kotlin/io/github/mscheong01/krotodc/template/TransformTemplateWithImports.kt: -------------------------------------------------------------------------------- 1 | // Copyright 2023 Minsoo Cheong 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 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 | package io.github.mscheong01.krotodc.template 15 | 16 | import io.github.mscheong01.krotodc.import.Import 17 | 18 | /** 19 | * A data class that contains a [TransformTemplate] and a set of [Import] imports. 20 | * The imports set must contain all imports required to use the template. 21 | */ 22 | data class TransformTemplateWithImports( 23 | val template: TransformTemplate, 24 | val imports: Set 25 | ) { 26 | companion object { 27 | fun of(template: TransformTemplate, imports: Set = setOf()) = 28 | TransformTemplateWithImports(template, imports = imports) 29 | fun of(template: String, imports: Set = setOf()) = 30 | TransformTemplateWithImports(TransformTemplate(template), imports = imports) 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /generator/src/test/proto/keyword.proto: -------------------------------------------------------------------------------- 1 | // Copyright 2023 Minsoo Cheong 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 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 | syntax = "proto3"; 15 | 16 | package com.example.keyword; 17 | 18 | option java_package = "io.github.mscheong01.keyword"; 19 | option java_multiple_files = true; 20 | 21 | message KeywordMessage { 22 | string in = 1; 23 | string fun = 2; 24 | string if = 3; 25 | string object = 4; 26 | oneof as { 27 | string typeof = 6; 28 | string while = 7; 29 | } 30 | repeated string for = 8; 31 | map else = 9; 32 | string public = 10; 33 | string package = 11; 34 | } 35 | 36 | message ProtobufJavaEscapedFieldMessage { 37 | string class = 1; 38 | } 39 | 40 | message ProtobufJavaEscapedRepeatedMessage { 41 | repeated string class = 1; 42 | } 43 | 44 | message ProtobufJavaEscapedMapMessage { 45 | map class = 1; 46 | } 47 | 48 | message ProtobufJavaEscapedOneOfMessage { 49 | oneof class { 50 | string name = 1; 51 | } 52 | } 53 | 54 | message ProtobufJavaEscapedOneOfFieldMessage { 55 | oneof name { 56 | string class = 1; 57 | } 58 | } 59 | 60 | //message ProtobufJavaEscapedEnumMessage { 61 | // enum class { 62 | // UNKNOWN = 0; 63 | // ENGINEER = 1; 64 | // PRODUCT_MANAGER = 2; 65 | // } 66 | //} 67 | -------------------------------------------------------------------------------- /generator/src/main/kotlin/io/github/mscheong01/krotodc/template/TransformTemplate.kt: -------------------------------------------------------------------------------- 1 | // Copyright 2023 Minsoo Cheong 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 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 | package io.github.mscheong01.krotodc.template 15 | 16 | import com.squareup.kotlinpoet.CodeBlock 17 | 18 | /** 19 | * A Transform Template is a kotlin poet string template that transforms a KrotoDC type to Protobuf Java type, or vice versa. 20 | * The template is a string that contains single or no placeholder (%L), which is the input. 21 | * This behavior is validated on instantiation. 22 | * 23 | * Example: 24 | * ``` 25 | * val transformTemplate = TransformTemplate("%L.toProto()") 26 | * val input = CodeBlock.of("java.time.LocalDateTime.now()") 27 | * val output = transformTemplate.safeCall(input) 28 | * // output == CodeBlock.of("java.time.LocalDateTime.now().toProto()") 29 | * ``` 30 | */ 31 | @JvmInline 32 | value class TransformTemplate(val value: String) { 33 | init { 34 | require(value.count { it == '%' } <= 1) { 35 | "TransformTemplate must contain single or no placeholder" 36 | } 37 | require(value.count { it == '%' } == 0 || value.contains("%L")) { 38 | "TransformTemplate must only containe the (%L) placeholder" 39 | } 40 | } 41 | fun safeCall(input: CodeBlock): CodeBlock = if (value.contains("%L")) { 42 | CodeBlock.of(value, input) 43 | } else { 44 | CodeBlock.of(value) 45 | } 46 | fun safeCall(input: String): CodeBlock = safeCall(CodeBlock.of(input)) 47 | } 48 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # CONTRIBUTING.md 2 | 3 | ## Contributing to krotoDC 4 | 5 | First of all, thank you for considering contributing to krotoDC! We appreciate your time and effort and are excited to have you join our community. This document serves as a guideline for contributing to our open-source library. 6 | 7 | ### Getting Started 8 | 9 | 1. Fork the repository on GitHub. 10 | 2. Clone your fork to your local machine. 11 | 3. Set up your development environment, ensuring that all necessary dependencies are installed. 12 | 4. Create a new branch for your feature or bugfix. 13 | 14 | ### Code Style 15 | 16 | We follow the [Kotlin Coding Conventions](https://kotlinlang.org/docs/coding-conventions.html) for code style. Please make sure your code adheres to these conventions. 17 | 18 | Additionally, before committing your code, ensure that the `ktlintCheck` passes: 19 | ```bash 20 | ./gradlew ktlintCheck 21 | ``` 22 | If there are any issues, you can try to fix them automatically by running: 23 | ```bash 24 | ./gradlew ktlintFormat 25 | ``` 26 | 27 | ### Tests 28 | 29 | When changing the generated code output, include a test for it in the same pull request (PR). Add new `.proto` files under `src/test/proto`, and they will be automatically generated and accessible from the tests. Ensure that all tests pass before submitting your PR: 30 | ```bash 31 | ./gradlew test 32 | ``` 33 | 34 | ### Pull Requests 35 | 36 | 1. Commit your changes to your feature or bugfix branch. 37 | 2. Push your branch to your fork on GitHub. 38 | 3. Create a pull request from your fork's branch to the original repository's `main` branch. 39 | 4. In the pull request description, provide a clear and concise description of the changes you made, and mention any issues that your PR addresses. 40 | 5. Await feedback from maintainers. They may request changes or ask questions. Be prepared to iterate on your PR based on their feedback. 41 | 42 | ### Code of Conduct 43 | 44 | We expect all contributors to adhere to the standard [Code of Conduct](https://www.contributor-covenant.org/version/2/0/code_of_conduct.html). Please treat everyone with respect and maintain a professional and inclusive environment. 45 | 46 | --- 47 | 48 | Once again, thank you for your interest in contributing to krotoDC. We look forward to your contributions and to working with you! 49 | -------------------------------------------------------------------------------- /generator/src/test/kotlin/io/github/mscheong01/krotodc/JsonConverterTest.kt: -------------------------------------------------------------------------------- 1 | // Copyright 2024 Minsoo Cheong 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 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 | package io.github.mscheong01.krotodc 15 | 16 | import com.example.defaultvaluestest.krotodc.DefaultValueNestedMessage 17 | import com.example.defaultvaluestest.krotodc.DefaultValueTest 18 | import com.example.defaultvaluestest.krotodc.defaultvaluetest.toJson 19 | import io.github.mscheong01.test.Person 20 | import io.github.mscheong01.test.krotodc.person.fromJson 21 | import io.github.mscheong01.test.krotodc.person.toDataClass 22 | import io.github.mscheong01.test.krotodc.person.toJson 23 | import org.assertj.core.api.Assertions 24 | import org.junit.jupiter.api.Test 25 | 26 | class JsonConverterTest { 27 | 28 | @Test 29 | fun `test simple message`() { 30 | val proto = Person.newBuilder() 31 | .setName("John") 32 | .setAge(30) 33 | .build() 34 | val dataClass = proto.toDataClass() 35 | val json = dataClass.toJson() 36 | Assertions.assertThat(json).isEqualTo( 37 | "{\n \"name\": \"John\",\n \"age\": 30\n}" 38 | ) 39 | val deserializedDataClass = io.github.mscheong01.test.krotodc.Person.fromJson(json) 40 | Assertions.assertThat(dataClass).isEqualTo(deserializedDataClass) 41 | } 42 | 43 | @Test 44 | fun `test including default values in toJson call`() { 45 | val proto = DefaultValueTest( 46 | intField = 0, 47 | stringList = emptyList(), 48 | defaultValueNestedMessage = DefaultValueNestedMessage( 49 | doubleField = 31.08, 50 | booleanField = false 51 | ) 52 | ) 53 | val json = proto.toJson(includeDefaultValues = true) 54 | Assertions.assertThat(json).isEqualTo( 55 | "{\"intField\":0,\"stringList\":[],\"defaultValueNestedMessage\":{\"doubleField\":31.08,\"booleanField\":false}}" 56 | ) 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /generator/src/main/kotlin/io/github/mscheong01/krotodc/specgenerators/function/ToJsonFunctionGenerator.kt: -------------------------------------------------------------------------------- 1 | // Copyright 2024 Minsoo Cheong 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 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 | package io.github.mscheong01.krotodc.specgenerators.function 15 | 16 | import com.google.protobuf.Descriptors.Descriptor 17 | import com.squareup.kotlinpoet.FunSpec 18 | import com.squareup.kotlinpoet.ParameterSpec 19 | import io.github.mscheong01.krotodc.import.FunSpecsWithImports 20 | import io.github.mscheong01.krotodc.import.Import 21 | import io.github.mscheong01.krotodc.specgenerators.FunSpecGenerator 22 | import io.github.mscheong01.krotodc.util.krotoDCTypeName 23 | 24 | class ToJsonFunctionGenerator : FunSpecGenerator { 25 | override fun generate(descriptor: Descriptor): FunSpecsWithImports { 26 | val imports = mutableSetOf() 27 | val generatedType = descriptor.krotoDCTypeName 28 | val functionBuilder = FunSpec.builder("toJson") 29 | .receiver(generatedType) 30 | .addParameter( 31 | ParameterSpec.builder("includeDefaultValues", Boolean::class) 32 | .defaultValue("%L", false) 33 | .build() 34 | ) 35 | .returns(String::class) 36 | functionBuilder.addCode( 37 | """ 38 | return JsonFormat.printer().let { defaultPrinter -> 39 | var printer = defaultPrinter 40 | if (includeDefaultValues) { 41 | printer = printer.alwaysPrintFieldsWithNoPresence().omittingInsignificantWhitespace() 42 | } 43 | printer 44 | }.print(this@toJson.toProto()) 45 | """.trimIndent() 46 | ) 47 | imports.add(Import("com.google.protobuf.util", listOf("JsonFormat"))) 48 | return FunSpecsWithImports( 49 | listOf(functionBuilder.build()), 50 | imports 51 | ) 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /example/build.gradle.kts: -------------------------------------------------------------------------------- 1 | import com.google.protobuf.gradle.id 2 | 3 | repositories { 4 | maven { 5 | url = uri("https://s01.oss.sonatype.org/content/repositories/snapshots/") 6 | } 7 | mavenLocal() 8 | } 9 | 10 | dependencies { 11 | // implementation(kotlin("stdlib")) 12 | // implementation("io.grpc:grpc-protobuf:${rootProject.ext["grpcJavaVersion"]}") 13 | // https://mvnrepository.com/artifact/com.google.protobuf/protobuf-java 14 | implementation("com.google.protobuf:protobuf-java:${rootProject.ext["protobufVersion"]}") 15 | implementation("com.google.protobuf:protobuf-java-util:${rootProject.ext["protobufVersion"]}") 16 | implementation("io.grpc:grpc-stub:${rootProject.ext["grpcJavaVersion"]}") 17 | implementation("io.grpc:grpc-kotlin-stub:${rootProject.ext["grpcKotlinVersion"]}") 18 | implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:${rootProject.ext["coroutinesVersion"]}") 19 | implementation("io.grpc:grpc-protobuf:${rootProject.ext["grpcJavaVersion"]}") 20 | implementation("io.github.mscheong01:krotoDC-core:1.2.2") 21 | runtimeOnly("io.grpc:grpc-netty:${rootProject.ext["grpcJavaVersion"]}") 22 | 23 | testImplementation("javax.annotation:javax.annotation-api:1.3.2") 24 | // https://mvnrepository.com/artifact/org.junit.jupiter/junit-jupiter-api 25 | testImplementation("org.junit.jupiter:junit-jupiter-api:5.9.2") 26 | testImplementation("org.junit.jupiter:junit-jupiter-engine:5.9.2") 27 | testRuntimeOnly("org.junit.platform:junit-platform-launcher") 28 | // https://mvnrepository.com/artifact/org.assertj/assertj-core 29 | testImplementation("org.assertj:assertj-core:3.24.2") 30 | testImplementation("io.grpc:grpc-testing:${rootProject.ext["grpcJavaVersion"]}") 31 | } 32 | 33 | protobuf { 34 | protoc { 35 | artifact = "com.google.protobuf:protoc:${rootProject.ext["protobufVersion"]}" 36 | } 37 | plugins { 38 | id("grpc") { 39 | artifact = "io.grpc:protoc-gen-grpc-java:${rootProject.ext["grpcJavaVersion"]}" 40 | } 41 | id("krotoDC") { 42 | artifact = "io.github.mscheong01:protoc-gen-krotoDC:1.2.2:jdk8@jar" 43 | } 44 | } 45 | generateProtoTasks { 46 | all().forEach { 47 | // if (it.name.startsWith("generateTestProto")) { 48 | // it.dependsOn("jar") 49 | // } 50 | 51 | it.plugins { 52 | id("grpc") 53 | id("krotoDC") 54 | } 55 | } 56 | } 57 | } 58 | 59 | tasks.withType { 60 | enabled = false 61 | } 62 | -------------------------------------------------------------------------------- /generator/src/main/kotlin/io/github/mscheong01/krotodc/specgenerators/function/FromJsonFunctionGenerator.kt: -------------------------------------------------------------------------------- 1 | // Copyright 2024 Minsoo Cheong 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 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 | package io.github.mscheong01.krotodc.specgenerators.function 15 | 16 | import com.google.protobuf.Descriptors.Descriptor 17 | import com.squareup.kotlinpoet.ClassName 18 | import com.squareup.kotlinpoet.FunSpec 19 | import com.squareup.kotlinpoet.ParameterSpec 20 | import io.github.mscheong01.krotodc.import.FunSpecsWithImports 21 | import io.github.mscheong01.krotodc.import.Import 22 | import io.github.mscheong01.krotodc.specgenerators.FunSpecGenerator 23 | import io.github.mscheong01.krotodc.util.krotoDCTypeName 24 | import io.github.mscheong01.krotodc.util.protobufJavaTypeName 25 | 26 | class FromJsonFunctionGenerator : FunSpecGenerator { 27 | override fun generate(descriptor: Descriptor): FunSpecsWithImports { 28 | val imports = mutableSetOf() 29 | val generatedType = descriptor.krotoDCTypeName 30 | val generatedTypeCompanion = ClassName( 31 | generatedType.packageName, 32 | *generatedType.simpleNames.toTypedArray(), 33 | "Companion" 34 | ) 35 | val protoType = descriptor.protobufJavaTypeName 36 | val functionBuilder = FunSpec.builder("fromJson") 37 | .addParameter( 38 | ParameterSpec.builder( 39 | "json", 40 | String::class 41 | ).build() 42 | ) 43 | .receiver(generatedTypeCompanion) 44 | .returns(generatedType) 45 | functionBuilder.addCode("return %L.newBuilder()\n", protoType) 46 | functionBuilder.addCode(" .apply {\n") 47 | functionBuilder.addCode(" JsonFormat.parser().ignoringUnknownFields().merge(json, this@apply)\n") 48 | functionBuilder.addCode(" }.build().toDataClass();\n") 49 | 50 | imports.add(Import("com.google.protobuf.util", listOf("JsonFormat"))) 51 | return FunSpecsWithImports( 52 | listOf(functionBuilder.build()), 53 | imports 54 | ) 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /generator/src/test/proto/test.proto: -------------------------------------------------------------------------------- 1 | // Copyright 2023 Minsoo Cheong 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 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 | syntax = "proto3"; 15 | 16 | package com.example.test; 17 | 18 | option java_package = "io.github.mscheong01.test"; 19 | option java_multiple_files = true; 20 | 21 | message Person { 22 | string name = 1; 23 | int32 age = 2; 24 | } 25 | 26 | enum Job { 27 | UNKNOWN = 0; 28 | ENGINEER = 1; 29 | PRODUCT_MANAGER = 2; 30 | } 31 | 32 | message Employee { 33 | string name = 1; 34 | int32 age = 2; 35 | Job job = 3; 36 | } 37 | 38 | message TopLevelMessage { 39 | string name = 1; 40 | NestedMessage nested_message = 2; 41 | NestedEnum nested_enum = 3; 42 | 43 | message NestedMessage { 44 | string name = 1; 45 | } 46 | 47 | enum NestedEnum { 48 | DEFAULT = 0; 49 | A = 1; 50 | B = 2; 51 | } 52 | } 53 | 54 | message PrimitiveMessage { 55 | int32 int32_field = 1; 56 | int64 int64_field = 2; 57 | uint32 uint32_field = 3; 58 | uint64 uint64_field = 4; 59 | sint32 sint32_field = 5; 60 | sint64 sint64_field = 6; 61 | fixed32 fixed32_field = 7; 62 | fixed64 fixed64_field = 8; 63 | sfixed32 sfixed32_field = 9; 64 | sfixed64 sfixed64_field = 10; 65 | float float_field = 11; 66 | double double_field = 12; 67 | bool bool_field = 13; 68 | string string_field = 14; 69 | bytes bytes_field = 15; 70 | } 71 | 72 | message RepeatedMessage { 73 | repeated string repeated_string = 1; 74 | repeated Person repeated_person = 2; 75 | repeated Job repeated_job = 3; 76 | } 77 | 78 | message MapMessage { 79 | map map_string_string = 1; 80 | map map_string_person = 2; 81 | map map_int_job = 3; 82 | } 83 | 84 | message OptionalMessage { 85 | optional string optional_string = 1; 86 | optional int32 optional_int = 2; 87 | optional Person optional_person = 3; 88 | } 89 | 90 | message OneOfMessage { 91 | oneof oneof_field { 92 | string oneof_string = 1; 93 | int32 oneof_int = 2; 94 | Person oneof_person = 3; 95 | } 96 | } 97 | 98 | message EmptyMessage { 99 | 100 | } 101 | 102 | message DeprecatedMessage { 103 | string name = 1 [deprecated = true]; 104 | oneof oneof_field { 105 | string oneof_string = 2 [deprecated = true]; 106 | int32 oneof_int = 3; 107 | Person oneof_person = 4; 108 | } 109 | } 110 | -------------------------------------------------------------------------------- /generator/src/main/kotlin/io/github/mscheong01/krotodc/util/FieldName.kt: -------------------------------------------------------------------------------- 1 | package io.github.mscheong01.krotodc.util 2 | 3 | // Protocol Buffers - Google's data interchange format 4 | // Copyright 2008 Google Inc. All rights reserved. 5 | // https://developers.google.com/protocol-buffers/ 6 | // 7 | // Redistribution and use in source and binary forms, with or without 8 | // modification, are permitted provided that the following conditions are 9 | // met: 10 | // 11 | // * Redistributions of source code must retain the above copyright 12 | // notice, this list of conditions and the following disclaimer. 13 | // * Redistributions in binary form must reproduce the above 14 | // copyright notice, this list of conditions and the following disclaimer 15 | // in the documentation and/or other materials provided with the 16 | // distribution. 17 | // * Neither the name of Google Inc. nor the names of its 18 | // contributors may be used to endorse or promote products derived from 19 | // this software without specific prior written permission. 20 | // 21 | // THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS 22 | // "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT 23 | // LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR 24 | // A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT 25 | // OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, 26 | // SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT 27 | // LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, 28 | // DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY 29 | // THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 30 | // (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 31 | // OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 32 | 33 | // This method was copied from protocolbuffers/protobuf: 34 | // https://github.com/protocolbuffers/protobuf/blob/8c8b2be3a830755014d338d023c8b60779f70b8b/java/core/src/main/java/com/google/protobuf/Descriptors.java 35 | // This method should match exactly with the ToJsonName() function in C++ 36 | // descriptor.cc. 37 | internal fun fieldNameToJsonName(name: String): String { 38 | val length = name.length 39 | val result = StringBuilder(length) 40 | var isNextUpperCase = false 41 | for (i in 0 until length) { 42 | var ch = name[i] 43 | if (ch == '_') { 44 | isNextUpperCase = true 45 | } else if (isNextUpperCase) { 46 | // This closely matches the logic for ASCII characters in: 47 | // http://google3/google/protobuf/descriptor.cc?l=249-251&rcl=228891689 48 | if ('a' <= ch && ch <= 'z') { 49 | ch = (ch - 'a' + 'A'.code).toChar() 50 | } 51 | result.append(ch) 52 | isNextUpperCase = false 53 | } else { 54 | result.append(ch) 55 | } 56 | } 57 | return result.toString() 58 | } 59 | -------------------------------------------------------------------------------- /generator/src/main/kotlin/io/github/mscheong01/krotodc/KrotoDCCodeGenerator.kt: -------------------------------------------------------------------------------- 1 | // Copyright 2023 Minsoo Cheong 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 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 | package io.github.mscheong01.krotodc 15 | 16 | import com.google.protobuf.Descriptors 17 | import com.google.protobuf.compiler.PluginProtos 18 | import com.squareup.kotlinpoet.FileSpec 19 | import io.github.mscheong01.krotodc.specgenerators.file.ConverterGenerator 20 | import io.github.mscheong01.krotodc.specgenerators.file.DataClassGenerator 21 | import io.github.mscheong01.krotodc.specgenerators.file.GrpcKrotoGenerator 22 | 23 | object KrotoDCCodeGenerator { 24 | 25 | val subGenerators = listOf( 26 | DataClassGenerator(), 27 | GrpcKrotoGenerator(), 28 | ConverterGenerator() 29 | ) 30 | 31 | fun generateCode(request: PluginProtos.CodeGeneratorRequest) { 32 | val fileNameToDescriptor = mutableMapOf() 33 | request.protoFileList 34 | .toList() 35 | .forEach { file -> 36 | val deps = file.dependencyList.map { dep -> 37 | fileNameToDescriptor[dep] 38 | ?: throw IllegalStateException("Dependency $dep not found for file ${file.name}") 39 | } 40 | fileNameToDescriptor[file.name] = 41 | Descriptors.FileDescriptor.buildFrom(file, deps.toTypedArray()) 42 | } 43 | 44 | fileNameToDescriptor.filterNot { (fileName, _) -> 45 | fileName.startsWith("google/") 46 | }.forEach { (_, descriptor) -> 47 | val responseBuilder = PluginProtos.CodeGeneratorResponse.newBuilder() 48 | .setSupportedFeatures(PluginProtos.CodeGeneratorResponse.Feature.FEATURE_PROTO3_OPTIONAL_VALUE.toLong()) 49 | responseBuilder.addAllFile( 50 | subGenerators.map { it.generate(descriptor) }.flatten() 51 | .let { kotlinPoetFileSpecToCodeGeneratorResponseFile(it) } 52 | ).build() 53 | .let { it.writeTo(System.out) } 54 | } 55 | } 56 | 57 | fun kotlinPoetFileSpecToCodeGeneratorResponseFile( 58 | fileSpecs: List 59 | ): List { 60 | return fileSpecs.map { fileSpec -> 61 | PluginProtos.CodeGeneratorResponse.File.newBuilder().also { 62 | it.name = fileSpec.packageName.replace('.', '/') + "/" + fileSpec.name 63 | it.content = fileSpec.toString() 64 | }.build() 65 | } 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # krotoDC 2 | 3 | krotoDC is a protoc plugin for generating Kotlin data classes and gRPC service/stub from a .proto input. This library makes it easy to work with Protocol Buffers and gRPC in your Kotlin projects. 4 | 5 | check out my [blog post](https://medium.com/@icycle0409/introducing-krotodc-use-protobuf-and-grpc-with-kotlin-dataclasses-3144d0b20032) that introduces krotoDC 6 | ## Features 7 | 8 | - Generates Kotlin data classes from .proto files 9 | - Generates Converter extension methods between generated classes and GeneratedMessageV3 classes 10 | - Generates gRPC service/stub based on the service definition in .proto files 11 | - Supports Kotlin specific features like nullable fields and sealed oneof classes: 12 | 13 | see the generated code spec [here](https://github.com/mscheong01/krotoDC/blob/main/generator/README.md) 14 | 15 | ## Installation 16 | 17 | In your project's `build.gradle.kts` file, add the following dependencies: 18 | 19 | ```kotlin 20 | dependencies { 21 | implementation("com.google.protobuf:protobuf-java:4.30.1") 22 | implementation("com.google.protobuf:protobuf-java-util:4.30.1") 23 | implementation("io.grpc:grpc-stub:1.70.0") 24 | implementation("io.grpc:grpc-kotlin-stub:1.4.0") 25 | implementation("io.github.mscheong01:krotoDC-core:1.2.2") 26 | } 27 | ``` 28 | 29 | ## Usage 30 | 31 | 1. Configure the protobuf plugin in your `build.gradle.kts` file: 32 | 33 | ```kotlin 34 | protobuf { 35 | protoc { 36 | artifact = "com.google.protobuf:protoc:4.30.1" 37 | } 38 | plugins { 39 | id("grpc") { 40 | artifact = "io.grpc:protoc-gen-grpc-java:1.70.1" 41 | } 42 | id("krotoDC") { 43 | artifact = "io.github.mscheong01:protoc-gen-krotoDC:1.2.2:jdk8@jar" 44 | } 45 | } 46 | generateProtoTasks { 47 | all().forEach { 48 | it.plugins { 49 | id("grpc") 50 | id("krotoDC") 51 | } 52 | } 53 | } 54 | } 55 | ``` 56 | 57 | 2. Create your `.proto` files with your message and service definitions. 58 | 59 | 3. Build your project, and the plugin will generate Kotlin data classes and gRPC service/stub based on the `.proto` files. 60 | 61 | ## Snapshot Versions 62 | krotoDC provides snapshot versions that are automatically released when changes are pushed to the main branch. 63 | The current snapshot version is the next minor version of the current release version with a -SNAPSHOT suffix. 64 | For example, if the current release is 1.2.3, the snapshot version will be 1.3.0-SNAPSHOT. 65 | 66 | To use snapshot versions, add the maven snapshot repository to your build configuration 67 | ```kotlin 68 | repositories { 69 | maven { 70 | url = uri("https://s01.oss.sonatype.org/content/repositories/snapshots/") 71 | } 72 | } 73 | ``` 74 | ## Contributing 75 | 76 | Contributions are welcome! Please see our [contributing guidelines](https://github.com/mscheong01/krotoDC/blob/main/CONTRIBUTING.md) for more information. 77 | 78 | ## License 79 | 80 | This project is licensed under the [Apache 2.0 License](https://github.com/mscheong01/krotoDC/blob/main/LICENSE). 81 | -------------------------------------------------------------------------------- /gradlew.bat: -------------------------------------------------------------------------------- 1 | @rem 2 | @rem Copyright 2015 the original author or authors. 3 | @rem 4 | @rem Licensed under the Apache License, Version 2.0 (the "License"); 5 | @rem you may not use this file except in compliance with the License. 6 | @rem You may obtain a copy of the License at 7 | @rem 8 | @rem https://www.apache.org/licenses/LICENSE-2.0 9 | @rem 10 | @rem Unless required by applicable law or agreed to in writing, software 11 | @rem distributed under the License is distributed on an "AS IS" BASIS, 12 | @rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | @rem See the License for the specific language governing permissions and 14 | @rem limitations under the License. 15 | @rem 16 | 17 | @if "%DEBUG%"=="" @echo off 18 | @rem ########################################################################## 19 | @rem 20 | @rem Gradle startup script for Windows 21 | @rem 22 | @rem ########################################################################## 23 | 24 | @rem Set local scope for the variables with windows NT shell 25 | if "%OS%"=="Windows_NT" setlocal 26 | 27 | set DIRNAME=%~dp0 28 | if "%DIRNAME%"=="" set DIRNAME=. 29 | set APP_BASE_NAME=%~n0 30 | set APP_HOME=%DIRNAME% 31 | 32 | @rem Resolve any "." and ".." in APP_HOME to make it shorter. 33 | for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi 34 | 35 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 36 | set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" 37 | 38 | @rem Find java.exe 39 | if defined JAVA_HOME goto findJavaFromJavaHome 40 | 41 | set JAVA_EXE=java.exe 42 | %JAVA_EXE% -version >NUL 2>&1 43 | if %ERRORLEVEL% equ 0 goto execute 44 | 45 | echo. 46 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 47 | echo. 48 | echo Please set the JAVA_HOME variable in your environment to match the 49 | echo location of your Java installation. 50 | 51 | goto fail 52 | 53 | :findJavaFromJavaHome 54 | set JAVA_HOME=%JAVA_HOME:"=% 55 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe 56 | 57 | if exist "%JAVA_EXE%" goto execute 58 | 59 | echo. 60 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 61 | echo. 62 | echo Please set the JAVA_HOME variable in your environment to match the 63 | echo location of your Java installation. 64 | 65 | goto fail 66 | 67 | :execute 68 | @rem Setup the command line 69 | 70 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar 71 | 72 | 73 | @rem Execute Gradle 74 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %* 75 | 76 | :end 77 | @rem End local scope for the variables with windows NT shell 78 | if %ERRORLEVEL% equ 0 goto mainEnd 79 | 80 | :fail 81 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of 82 | rem the _cmd.exe /c_ return code! 83 | set EXIT_CODE=%ERRORLEVEL% 84 | if %EXIT_CODE% equ 0 set EXIT_CODE=1 85 | if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE% 86 | exit /b %EXIT_CODE% 87 | 88 | :mainEnd 89 | if "%OS%"=="Windows_NT" endlocal 90 | 91 | :omega 92 | -------------------------------------------------------------------------------- /generator/src/main/kotlin/io/github/mscheong01/krotodc/specgenerators/file/GrpcKrotoGenerator.kt: -------------------------------------------------------------------------------- 1 | // Copyright 2023 Minsoo Cheong 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 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 | package io.github.mscheong01.krotodc.specgenerators.file 15 | 16 | import com.google.protobuf.Descriptors 17 | import com.google.protobuf.Descriptors.ServiceDescriptor 18 | import com.squareup.kotlinpoet.FileSpec 19 | import com.squareup.kotlinpoet.TypeSpec 20 | import io.github.mscheong01.krotodc.specgenerators.FileSpecGenerator 21 | import io.github.mscheong01.krotodc.specgenerators.TypeSpecGenerator 22 | import io.github.mscheong01.krotodc.specgenerators.type.ClientStubGenerator 23 | import io.github.mscheong01.krotodc.specgenerators.type.ServiceImplBaseGenerator 24 | import io.github.mscheong01.krotodc.util.addAllImports 25 | import io.github.mscheong01.krotodc.util.krotoDCPackage 26 | 27 | class GrpcKrotoGenerator : FileSpecGenerator { 28 | override fun generate(fileDescriptor: Descriptors.FileDescriptor): List { 29 | val fileSpecs = mutableListOf() 30 | fileDescriptor.services.forEach { service -> 31 | val generators: List> = listOf( 32 | ServiceImplBaseGenerator(), 33 | ClientStubGenerator() 34 | ) 35 | val results = generators.map { 36 | it.generate(service) 37 | } 38 | val krotoGrpcClassName = service.name + "GrpcKroto" 39 | fileSpecs.add( 40 | FileSpec.builder(fileDescriptor.krotoDCPackage, krotoGrpcClassName + ".kt") 41 | .addType( 42 | TypeSpec.objectBuilder(krotoGrpcClassName) 43 | .apply { 44 | results.map { 45 | it.typeSpecs 46 | }.flatten().forEach { 47 | addType(it) 48 | } 49 | }.build() 50 | ).apply { 51 | if ( 52 | service.methods.any { 53 | it.isServerStreaming || it.isClientStreaming 54 | } 55 | ) { 56 | addImport("kotlinx.coroutines.flow", "map") 57 | } 58 | addAllImports( 59 | results.map { 60 | it.imports 61 | }.flatten().toSet() 62 | ) 63 | } 64 | .build() 65 | ) 66 | } 67 | 68 | return fileSpecs 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /.github/workflows/publish-release.yml: -------------------------------------------------------------------------------- 1 | name: Publish Release 2 | on: 3 | release: 4 | types: [ published ] 5 | branches: [ main ] 6 | workflow_dispatch: 7 | 8 | env: 9 | MAVEN_CENTRAL_USERNAME: ${{ secrets.MAVEN_CENTRAL_USERNAME }} 10 | MAVEN_CENTRAL_PASSWORD: ${{ secrets.MAVEN_CENTRAL_PASSWORD }} 11 | ORG_GRADLE_PROJECT_signingInMemoryKey: ${{ secrets.OSSRH_GPG_SECRET_KEY }} 12 | ORG_GRADLE_PROJECT_signingInMemoryKeyPassword: ${{ secrets.OSSRH_GPG_SECRET_KEY_PASSWORD }} 13 | 14 | jobs: 15 | deploy: 16 | name: Deploy 17 | runs-on: ubuntu-latest 18 | steps: 19 | - name: Checkout 20 | uses: actions/checkout@v3 21 | with: 22 | fetch-depth: 0 23 | token: ${{ secrets.PAT }} 24 | 25 | - name: Get Previous tag 26 | id: previous_tag 27 | uses: WyriHaximus/github-action-get-previous-tag@v1 28 | env: 29 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 30 | 31 | - name: Get next minor version 32 | id: semvers 33 | uses: WyriHaximus/github-action-next-semvers@v1 34 | with: 35 | version: ${{ steps.previous_tag.outputs.tag }} 36 | 37 | - name: Print semver 38 | id: print_semver 39 | run: | 40 | echo "current: $CURRENT" 41 | echo "next: $NEXT" 42 | env: 43 | CURRENT: ${{ steps.previous_tag.outputs.tag }} 44 | NEXT: ${{ steps.semvers.outputs.minor }} 45 | 46 | - name: Setup JDK 47 | uses: joschi/setup-jdk@v2 48 | with: 49 | java-version: '17' 50 | 51 | - name: Test 52 | working-directory: ./ 53 | run: ./gradlew test -PreleaseVersion=${{ steps.previous_tag.outputs.tag }} 54 | 55 | - name: Publish Test Report 56 | uses: mikepenz/action-junit-report@v2 57 | if: always() 58 | with: 59 | report_paths: '**/build/test-results/test/TEST-*.xml' 60 | 61 | - name: Publish to Maven Central 62 | working-directory: ./ 63 | run: ./gradlew publishToMavenCentral --no-configuration-cache -PreleaseVersion=${{ steps.previous_tag.outputs.tag }} 64 | env: 65 | ORG_GRADLE_PROJECT_mavenCentralUsername: ${{ secrets.MAVEN_CENTRAL_USERNAME }} 66 | ORG_GRADLE_PROJECT_mavenCentralPassword: ${{ secrets.MAVEN_CENTRAL_PASSWORD }} 67 | 68 | - name: Update library version 69 | working-directory: ./ 70 | run: ./gradlew updateVersion -Pnext=${{ steps.semvers.outputs.minor }}-SNAPSHOT 71 | 72 | - name: Commit gradle.properties with updated library version 73 | uses: stefanzweifel/git-auto-commit-action@v4 74 | with: 75 | commit_message: Update version to ${{ steps.semvers.outputs.minor }}-SNAPSHOT 76 | file_pattern: gradle.properties 77 | branch: main 78 | commit_user_name: version-control-bot 79 | commit_user_email: version-control-bot@users.noreply.github.com 80 | commit_author: version-control-bot 81 | 82 | - name: Create new milestone 83 | id: create_milestone 84 | uses: WyriHaximus/github-action-create-milestone@v1 85 | with: 86 | title: ${{ steps.semvers.outputs.minor }} 87 | env: 88 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} -------------------------------------------------------------------------------- /core/src/main/kotlin/io/github/mscheong01/krotodc/Extensions.kt: -------------------------------------------------------------------------------- 1 | // Copyright 2023 Minsoo Cheong 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 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 | package io.github.mscheong01.krotodc 15 | 16 | import com.google.protobuf.BoolValue 17 | import com.google.protobuf.ByteString 18 | import com.google.protobuf.BytesValue 19 | import com.google.protobuf.DoubleValue 20 | import com.google.protobuf.FloatValue 21 | import com.google.protobuf.Int32Value 22 | import com.google.protobuf.Int64Value 23 | import com.google.protobuf.StringValue 24 | import com.google.protobuf.Timestamp 25 | import com.google.protobuf.UInt32Value 26 | import com.google.protobuf.UInt64Value 27 | import java.time.Duration 28 | import java.time.Instant 29 | import java.time.LocalDateTime 30 | import java.time.ZoneOffset 31 | 32 | fun Timestamp.toLocalDateTime(): LocalDateTime { 33 | val instant = this.toInstant() 34 | return LocalDateTime.ofInstant(instant, ZoneOffset.UTC) 35 | } 36 | 37 | fun Timestamp.toInstant(): Instant { 38 | return Instant.ofEpochSecond(this.seconds, this.nanos.toLong()) 39 | } 40 | 41 | fun LocalDateTime.toProtoTimestamp(): Timestamp { 42 | return this.atZone(ZoneOffset.UTC).toInstant().toProtoTimestamp() 43 | } 44 | 45 | fun Instant.toProtoTimestamp(): Timestamp { 46 | return Timestamp.newBuilder().setSeconds(this.epochSecond) 47 | .setNanos(this.nano) 48 | .build() 49 | } 50 | 51 | fun com.google.protobuf.Duration.toDuration(): Duration { 52 | return Duration.ofSeconds(this.seconds, this.nanos.toLong()) 53 | } 54 | 55 | fun Duration.toProtoDuration(): com.google.protobuf.Duration { 56 | return com.google.protobuf.Duration.newBuilder() 57 | .setSeconds(this.seconds) 58 | .setNanos(this.nano) 59 | .build() 60 | } 61 | 62 | // Convert kotlin Double to protobuf DoubleValue 63 | fun Double.toDoubleValue(): DoubleValue = DoubleValue.newBuilder().setValue(this).build() 64 | 65 | // Convert kotlin Float to protobuf FloatValue 66 | fun Float.toFloatValue(): FloatValue = FloatValue.newBuilder().setValue(this).build() 67 | 68 | // Convert kotlin Long to protobuf Int64Value 69 | fun Long.toInt64Value(): Int64Value = Int64Value.newBuilder().setValue(this).build() 70 | 71 | // Convert kotlin Long to protobuf UInt64Value 72 | fun Long.toUInt64Value(): UInt64Value = UInt64Value.newBuilder().setValue(this).build() 73 | 74 | // Convert kotlin Int to protobuf Int32Value 75 | fun Int.toInt32Value(): Int32Value = Int32Value.newBuilder().setValue(this).build() 76 | 77 | // Convert kotlin Int to protobuf UInt32Value 78 | fun Int.toUInt32Value(): UInt32Value = UInt32Value.newBuilder().setValue(this).build() 79 | 80 | // Convert kotlin Boolean to protobuf BoolValue 81 | fun Boolean.toBoolValue(): BoolValue = BoolValue.newBuilder().setValue(this).build() 82 | 83 | // Convert kotlin String to protobuf StringValue 84 | fun String.toStringValue(): StringValue = StringValue.newBuilder().setValue(this).build() 85 | 86 | // Convert kotlin ByteString to protobuf BytesValue 87 | fun ByteString.toBytesValue(): BytesValue = BytesValue.newBuilder().setValue(this).build() 88 | -------------------------------------------------------------------------------- /generator/src/test/proto/rpc.proto: -------------------------------------------------------------------------------- 1 | // Copyright 2023 Minsoo Cheong 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 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 | syntax = "proto3"; 15 | 16 | import "google/protobuf/wrappers.proto"; 17 | import "google/protobuf/timestamp.proto"; 18 | import "google/protobuf/duration.proto"; 19 | import "google/protobuf/struct.proto"; 20 | import "google/protobuf/any.proto"; 21 | import "google/protobuf/empty.proto"; 22 | 23 | package com.example.rpc; 24 | 25 | option java_package = "io.github.mscheong01.rpc"; 26 | 27 | service RpcService { 28 | 29 | // unary rpc 30 | rpc UnaryTest(UnaryRequest) returns (UnaryResponse) {} 31 | 32 | // server streaming rpc 33 | rpc ServerStreamingTest(ServerStreamingRequest) returns (stream ServerStreamingResponse) {} 34 | 35 | // client streaming rpc 36 | rpc ClientStreamingTest(stream ClientStreamingRequest) returns (ClientStreamingResponse) {} 37 | 38 | // bidirectional streaming rpc 39 | rpc BidirectionalStreamingTest(stream BidirectionalStreamingRequest) returns (stream BidirectionalStreamingResponse) {} 40 | 41 | } 42 | 43 | service CommonMessageUseService { 44 | rpc UnaryRPC(CommonRequest) returns (CommonResponse) {} 45 | rpc ServerStreamingRPC(CommonRequest) returns (stream CommonResponse) {} 46 | rpc ClientStreamingRPC(stream CommonRequest) returns (CommonResponse) {} 47 | rpc BidirectionalStreamingRPC(stream CommonRequest) returns (stream CommonResponse) {} 48 | } 49 | 50 | service WellKnownTypeUseService { 51 | rpc EchoEmpty(google.protobuf.Empty) returns (google.protobuf.Empty) {} 52 | rpc EchoString(google.protobuf.StringValue) returns (google.protobuf.StringValue) {} 53 | rpc EchoBytes(google.protobuf.BytesValue) returns (google.protobuf.BytesValue) {} 54 | rpc EchoInt32(google.protobuf.Int32Value) returns (google.protobuf.Int32Value) {} 55 | rpc EchoInt64(google.protobuf.Int64Value) returns (google.protobuf.Int64Value) {} 56 | rpc EchoUInt32(google.protobuf.UInt32Value) returns (google.protobuf.UInt32Value) {} 57 | rpc EchoUInt64(google.protobuf.UInt64Value) returns (google.protobuf.UInt64Value) {} 58 | rpc EchoFloat(google.protobuf.FloatValue) returns (google.protobuf.FloatValue) {} 59 | rpc EchoDouble(google.protobuf.DoubleValue) returns (google.protobuf.DoubleValue) {} 60 | rpc EchoBool(google.protobuf.BoolValue) returns (google.protobuf.BoolValue) {} 61 | rpc EchoTimestamp(google.protobuf.Timestamp) returns (google.protobuf.Timestamp) {} 62 | rpc EchoDuration(google.protobuf.Duration) returns (google.protobuf.Duration) {} 63 | rpc EchoStruct(google.protobuf.Struct) returns (google.protobuf.Struct) {} 64 | rpc EchoAny(google.protobuf.Any) returns (google.protobuf.Any) {} 65 | } 66 | 67 | message UnaryRequest { 68 | string name = 1; 69 | } 70 | 71 | message UnaryResponse { 72 | string message = 1; 73 | } 74 | 75 | message ServerStreamingRequest { 76 | string name = 1; 77 | } 78 | 79 | message ServerStreamingResponse { 80 | string message = 1; 81 | } 82 | 83 | message ClientStreamingRequest { 84 | string name = 1; 85 | } 86 | 87 | message ClientStreamingResponse { 88 | string message = 1; 89 | } 90 | 91 | message BidirectionalStreamingRequest { 92 | string name = 1; 93 | } 94 | 95 | message BidirectionalStreamingResponse { 96 | string message = 1; 97 | } 98 | 99 | message CommonRequest { 100 | string name = 1; 101 | } 102 | 103 | message CommonResponse { 104 | string message = 1; 105 | } 106 | -------------------------------------------------------------------------------- /generator/src/test/proto/well_known_types.proto: -------------------------------------------------------------------------------- 1 | // Copyright 2023 Minsoo Cheong 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 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 | syntax = "proto3"; 15 | 16 | import "google/protobuf/wrappers.proto"; 17 | import "google/protobuf/timestamp.proto"; 18 | import "google/protobuf/duration.proto"; 19 | import "google/protobuf/struct.proto"; 20 | import "google/protobuf/any.proto"; 21 | import "google/protobuf/empty.proto"; 22 | 23 | package com.example.wellknowntypes; 24 | 25 | option java_package = "io.github.mscheong01.wellknowntypes"; 26 | option java_multiple_files = true; 27 | 28 | message MessageWithEmpty { 29 | google.protobuf.Empty empty = 1; 30 | } 31 | 32 | message TimeMessage { 33 | google.protobuf.Timestamp timestamp = 1; 34 | google.protobuf.Duration duration = 2; 35 | } 36 | 37 | message WrapperMessage { 38 | google.protobuf.DoubleValue double_value = 1; 39 | google.protobuf.FloatValue float_value = 2; 40 | google.protobuf.Int64Value int64_value = 3; 41 | google.protobuf.UInt64Value uint64_value = 4; 42 | google.protobuf.Int32Value int32_value = 5; 43 | google.protobuf.UInt32Value uint32_value = 6; 44 | google.protobuf.BoolValue bool_value = 7; 45 | google.protobuf.StringValue string_value = 8; 46 | google.protobuf.BytesValue bytes_value = 9; 47 | } 48 | 49 | message StructAnyMessage { 50 | google.protobuf.Struct struct = 1; 51 | google.protobuf.Any any = 2; 52 | } 53 | 54 | // well known types as repeated 55 | message RepeatedEmptyMessage { 56 | repeated google.protobuf.Empty empty = 1; 57 | } 58 | 59 | message RepeatedTimeMessage { 60 | repeated google.protobuf.Timestamp timestamp = 1; 61 | repeated google.protobuf.Duration duration = 2; 62 | } 63 | 64 | message RepeatedWrapperMessage { 65 | repeated google.protobuf.DoubleValue double_value = 1; 66 | repeated google.protobuf.FloatValue float_value = 2; 67 | repeated google.protobuf.Int64Value int64_value = 3; 68 | repeated google.protobuf.UInt64Value uint64_value = 4; 69 | repeated google.protobuf.Int32Value int32_value = 5; 70 | repeated google.protobuf.UInt32Value uint32_value = 6; 71 | repeated google.protobuf.BoolValue bool_value = 7; 72 | repeated google.protobuf.StringValue string_value = 8; 73 | repeated google.protobuf.BytesValue bytes_value = 9; 74 | } 75 | 76 | message RepeatedStructAnyMessage { 77 | repeated google.protobuf.Struct struct = 1; 78 | repeated google.protobuf.Any any = 2; 79 | } 80 | 81 | // well known types as map values 82 | message MapEmptyMessage { 83 | map empty = 1; 84 | } 85 | 86 | message MapTimeMessage { 87 | map timestamp = 1; 88 | map duration = 2; 89 | } 90 | 91 | message MapWrapperMessage { 92 | map double_value = 1; 93 | map float_value = 2; 94 | map int64_value = 3; 95 | map uint64_value = 4; 96 | map int32_value = 5; 97 | map uint32_value = 6; 98 | map bool_value = 7; 99 | map string_value = 8; 100 | map bytes_value = 9; 101 | } 102 | 103 | message MapStructAnyMessage { 104 | map struct = 1; 105 | map any = 2; 106 | } 107 | -------------------------------------------------------------------------------- /generator/src/main/kotlin/io/github/mscheong01/krotodc/specgenerators/file/ConverterGenerator.kt: -------------------------------------------------------------------------------- 1 | // Copyright 2023 Minsoo Cheong 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 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 | package io.github.mscheong01.krotodc.specgenerators.file 15 | 16 | import com.google.protobuf.Descriptors 17 | import com.squareup.kotlinpoet.FileSpec 18 | import com.squareup.kotlinpoet.FunSpec 19 | import io.github.mscheong01.krotodc.import.FunSpecsWithImports 20 | import io.github.mscheong01.krotodc.import.Import 21 | import io.github.mscheong01.krotodc.specgenerators.FileSpecGenerator 22 | import io.github.mscheong01.krotodc.specgenerators.function.FromJsonFunctionGenerator 23 | import io.github.mscheong01.krotodc.specgenerators.function.MessageToDataClassFunctionGenerator 24 | import io.github.mscheong01.krotodc.specgenerators.function.MessageToProtoFunctionGenerator 25 | import io.github.mscheong01.krotodc.specgenerators.function.ToJsonFunctionGenerator 26 | import io.github.mscheong01.krotodc.util.addAllImports 27 | import io.github.mscheong01.krotodc.util.isPredefinedType 28 | import io.github.mscheong01.krotodc.util.krotoDCPackage 29 | 30 | class ConverterGenerator : FileSpecGenerator { 31 | 32 | val messageToDataClassGenerator = MessageToDataClassFunctionGenerator() 33 | val messageToProtoGenerator = MessageToProtoFunctionGenerator() 34 | val toJsonFunctionGenerator = ToJsonFunctionGenerator() 35 | val fromJsonFunctionGenerator = FromJsonFunctionGenerator() 36 | 37 | override fun generate(fileDescriptor: Descriptors.FileDescriptor): List { 38 | val fileSpecs = mutableMapOf() 39 | for (messageType in fileDescriptor.messageTypes) { 40 | val packageValue = messageType.file.krotoDCPackage + '.' + messageType.name.lowercase() 41 | val fileBuilder = FileSpec 42 | .builder(packageValue, "${messageType.name}Converters.kt") 43 | val (funSpecs, imports) = generateConvertersForMessageDescriptor(messageType) 44 | funSpecs.forEach { fileBuilder.addFunction(it) } 45 | fileBuilder.addAllImports(imports) 46 | fileSpecs[messageType.fullName] = fileBuilder.build() 47 | } 48 | return fileSpecs.values.toList() 49 | } 50 | 51 | fun generateConvertersForMessageDescriptor( 52 | messageDescriptor: Descriptors.Descriptor 53 | ): FunSpecsWithImports { 54 | if ( 55 | messageDescriptor.isPredefinedType() || 56 | messageDescriptor.options.mapEntry 57 | ) { 58 | return FunSpecsWithImports.EMPTY 59 | } 60 | val funSpecs = mutableListOf() 61 | val imports = mutableSetOf() 62 | 63 | messageToDataClassGenerator.generate(messageDescriptor).let { 64 | funSpecs.addAll(it.funSpecs) 65 | imports.addAll(it.imports) 66 | } 67 | messageToProtoGenerator.generate(messageDescriptor).let { 68 | funSpecs.addAll(it.funSpecs) 69 | imports.addAll(it.imports) 70 | } 71 | toJsonFunctionGenerator.generate(messageDescriptor).let { 72 | funSpecs.addAll(it.funSpecs) 73 | imports.addAll(it.imports) 74 | } 75 | fromJsonFunctionGenerator.generate(messageDescriptor).let { 76 | funSpecs.addAll(it.funSpecs) 77 | imports.addAll(it.imports) 78 | } 79 | 80 | messageDescriptor.nestedTypes.forEach { nestedType -> 81 | generateConvertersForMessageDescriptor(nestedType).let { 82 | funSpecs.addAll(it.funSpecs) 83 | imports.addAll(it.imports) 84 | } 85 | } 86 | 87 | return FunSpecsWithImports( 88 | funSpecs = funSpecs, 89 | imports = imports 90 | ) 91 | } 92 | } 93 | -------------------------------------------------------------------------------- /generator/build.gradle.kts: -------------------------------------------------------------------------------- 1 | import com.google.protobuf.gradle.id 2 | 3 | plugins { 4 | application 5 | } 6 | 7 | application { 8 | mainClass.set("io.github.mscheong01.krotodc.MainExecutor") 9 | } 10 | 11 | dependencies { 12 | // implementation(kotlin("stdlib")) 13 | // implementation("io.grpc:grpc-protobuf:${rootProject.ext["grpcJavaVersion"]}") 14 | // https://mvnrepository.com/artifact/com.google.protobuf/protobuf-java 15 | implementation("com.google.protobuf:protobuf-java:${rootProject.ext["protobufVersion"]}") 16 | implementation("com.google.protobuf:protobuf-java-util:${rootProject.ext["protobufVersion"]}") 17 | 18 | implementation(kotlin("reflect")) 19 | implementation("com.squareup:kotlinpoet:${rootProject.ext["kotlinPoetVersion"]}") 20 | // https://mvnrepository.com/artifact/com.squareup.okhttp/okhttp 21 | implementation("com.squareup.okhttp:okhttp:2.7.5") 22 | // https://mvnrepository.com/artifact/com.fasterxml.jackson.core/jackson-databind 23 | implementation("com.fasterxml.jackson.core:jackson-databind:2.14.2") 24 | // https://mvnrepository.com/artifact/com.fasterxml.jackson.module/jackson-module-kotlin 25 | implementation("com.fasterxml.jackson.module:jackson-module-kotlin:2.14.2") 26 | 27 | implementation("io.grpc:grpc-stub:${rootProject.ext["grpcJavaVersion"]}") 28 | implementation("io.grpc:grpc-kotlin-stub:${rootProject.ext["grpcKotlinVersion"]}") 29 | implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:${rootProject.ext["coroutinesVersion"]}") 30 | implementation(project(":core")) 31 | testImplementation("io.grpc:grpc-protobuf:${rootProject.ext["grpcJavaVersion"]}") 32 | 33 | testImplementation("javax.annotation:javax.annotation-api:1.3.2") 34 | // https://mvnrepository.com/artifact/org.junit.jupiter/junit-jupiter-api 35 | testImplementation("org.junit.jupiter:junit-jupiter-api:5.9.2") 36 | testImplementation("org.junit.jupiter:junit-jupiter-engine:5.9.2") 37 | testRuntimeOnly("org.junit.platform:junit-platform-launcher") 38 | // https://mvnrepository.com/artifact/org.assertj/assertj-core 39 | testImplementation("org.assertj:assertj-core:3.24.2") 40 | testImplementation("io.grpc:grpc-inprocess:${rootProject.ext["grpcJavaVersion"]}") 41 | testImplementation("io.grpc:grpc-testing:${rootProject.ext["grpcJavaVersion"]}") 42 | } 43 | 44 | configure { 45 | coordinates( 46 | groupId = project.group.toString(), 47 | artifactId = "protoc-gen-krotoDC", 48 | version = project.version.toString() 49 | ) 50 | 51 | pom { 52 | name.set("krotoDC") 53 | description.set( 54 | "protoc-gen-krotoDC is a protoc plugin for generating kotlin data classes and " + 55 | "grpc service/stub from a .proto input." 56 | ) 57 | } 58 | } 59 | 60 | tasks.jar { 61 | println(application.mainClass.get()) 62 | manifest { 63 | attributes["Main-Class"] = application.mainClass.get() 64 | } 65 | 66 | from(sourceSets.main.get().output) 67 | 68 | val runtimeClasspathJars = configurations.runtimeClasspath.get().filter { it.name.endsWith(".jar") } 69 | from(runtimeClasspathJars.map { zipTree(it) }) 70 | 71 | duplicatesStrategy = DuplicatesStrategy.INCLUDE 72 | 73 | archiveClassifier = "jdk8" 74 | } 75 | 76 | protobuf { 77 | 78 | protoc { 79 | artifact = "com.google.protobuf:protoc:${rootProject.ext["protobufVersion"]}" 80 | } 81 | plugins { 82 | id("grpc") { 83 | artifact = "io.grpc:protoc-gen-grpc-java:${rootProject.ext["grpcJavaVersion"]}" 84 | } 85 | id("grpckt") { 86 | artifact = "io.grpc:protoc-gen-grpc-kotlin:${rootProject.ext["grpcKotlinVersion"]}:jdk8@jar" 87 | } 88 | id("krotoDC") { 89 | path = tasks.jar.get().archiveFile.get().asFile.absolutePath 90 | } 91 | } 92 | generateProtoTasks { 93 | all().forEach { 94 | if (it.name.startsWith("generateTestProto")) { 95 | it.dependsOn("jar") 96 | } 97 | 98 | it.plugins { 99 | id("grpc") 100 | id("grpckt") 101 | id("krotoDC") 102 | } 103 | } 104 | } 105 | } 106 | -------------------------------------------------------------------------------- /generator/src/test/kotlin/io/github/mscheong01/krotodc/RpcTest.kt: -------------------------------------------------------------------------------- 1 | // Copyright 2023 Minsoo Cheong 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 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 | package io.github.mscheong01.krotodc 15 | 16 | import io.github.mscheong01.rpc.krotodc.BidirectionalStreamingRequest 17 | import io.github.mscheong01.rpc.krotodc.BidirectionalStreamingResponse 18 | import io.github.mscheong01.rpc.krotodc.ClientStreamingRequest 19 | import io.github.mscheong01.rpc.krotodc.ClientStreamingResponse 20 | import io.github.mscheong01.rpc.krotodc.RpcServiceGrpcKroto 21 | import io.github.mscheong01.rpc.krotodc.ServerStreamingRequest 22 | import io.github.mscheong01.rpc.krotodc.ServerStreamingResponse 23 | import io.github.mscheong01.rpc.krotodc.UnaryRequest 24 | import io.github.mscheong01.rpc.krotodc.UnaryResponse 25 | import io.grpc.inprocess.InProcessChannelBuilder 26 | import io.grpc.inprocess.InProcessServerBuilder 27 | import io.grpc.testing.GrpcCleanupRule 28 | import kotlinx.coroutines.flow.Flow 29 | import kotlinx.coroutines.flow.flowOf 30 | import kotlinx.coroutines.flow.map 31 | import kotlinx.coroutines.flow.toList 32 | import kotlinx.coroutines.runBlocking 33 | import org.junit.Rule 34 | import org.junit.jupiter.api.Assertions.assertEquals 35 | import org.junit.jupiter.api.Test 36 | 37 | class RpcTest { 38 | 39 | @JvmField 40 | @Rule 41 | val grpcCleanup = GrpcCleanupRule() 42 | 43 | private val serviceImpl = object : RpcServiceGrpcKroto.RpcServiceCoroutineImplBase() { 44 | override suspend fun unaryTest(request: UnaryRequest): UnaryResponse { 45 | return UnaryResponse(request.name.reversed()) 46 | } 47 | 48 | override fun serverStreamingTest(request: ServerStreamingRequest) = 49 | flowOf(ServerStreamingResponse(request.name.reversed())) 50 | 51 | override suspend fun clientStreamingTest(requests: Flow): ClientStreamingResponse { 52 | val messages = requests.toList().joinToString(separator = ", ") { it.name } 53 | return ClientStreamingResponse(messages) 54 | } 55 | 56 | override fun bidirectionalStreamingTest(requests: Flow) = 57 | requests.map { BidirectionalStreamingResponse(it.name.reversed()) } 58 | } 59 | 60 | private val serverName = InProcessServerBuilder.generateName() 61 | 62 | init { 63 | grpcCleanup.register( 64 | InProcessServerBuilder.forName(serverName).directExecutor().addService(serviceImpl).build().start() 65 | ) 66 | } 67 | 68 | private val channel = 69 | grpcCleanup.register(InProcessChannelBuilder.forName(serverName).directExecutor().build()) 70 | private val stub = RpcServiceGrpcKroto.RpcServiceCoroutineStub(channel) 71 | 72 | @Test 73 | fun `unaryTest returns reversed message`() = runBlocking { 74 | val request = UnaryRequest("Hello") 75 | val response = stub.unaryTest(request) 76 | assertEquals("olleH", response.message) 77 | } 78 | 79 | @Test 80 | fun `serverStreamingTest returns reversed message`() = runBlocking { 81 | val request = ServerStreamingRequest("World") 82 | val responseList = stub.serverStreamingTest(request).toList() 83 | assertEquals(1, responseList.size) 84 | assertEquals("dlroW", responseList[0].message) 85 | } 86 | 87 | @Test 88 | fun `clientStreamingTest returns concatenated messages`() = runBlocking { 89 | val requests = flowOf( 90 | ClientStreamingRequest("One"), 91 | ClientStreamingRequest("Two"), 92 | ClientStreamingRequest("Three") 93 | ) 94 | val response = stub.clientStreamingTest(requests) 95 | assertEquals("One, Two, Three", response.message) 96 | } 97 | 98 | @Test 99 | fun `bidirectionalStreamingTest returns reversed messages`() = runBlocking { 100 | val requests = flowOf( 101 | BidirectionalStreamingRequest("One"), 102 | BidirectionalStreamingRequest("Two"), 103 | BidirectionalStreamingRequest("Three") 104 | ) 105 | val responseList = stub.bidirectionalStreamingTest(requests).toList() 106 | assertEquals(3, responseList.size) 107 | assertEquals("enO", responseList[0].message) 108 | assertEquals("owT", responseList[1].message) 109 | assertEquals("eerhT", responseList[2].message) 110 | } 111 | } 112 | -------------------------------------------------------------------------------- /generator/src/test/kotlin/io/github/mscheong01/krotodc/KeywordEscapeTest.kt: -------------------------------------------------------------------------------- 1 | // Copyright 2023 Minsoo Cheong 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 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 | package io.github.mscheong01.krotodc 15 | 16 | import io.github.mscheong01.keyword.KeywordMessage 17 | import io.github.mscheong01.keyword.ProtobufJavaEscapedFieldMessage 18 | import io.github.mscheong01.keyword.ProtobufJavaEscapedMapMessage 19 | import io.github.mscheong01.keyword.ProtobufJavaEscapedOneOfFieldMessage 20 | import io.github.mscheong01.keyword.ProtobufJavaEscapedOneOfMessage 21 | import io.github.mscheong01.keyword.ProtobufJavaEscapedRepeatedMessage 22 | import io.github.mscheong01.keyword.krotodc.keywordmessage.toDataClass 23 | import io.github.mscheong01.keyword.krotodc.keywordmessage.toProto 24 | import io.github.mscheong01.keyword.krotodc.protobufjavaescapedfieldmessage.toDataClass 25 | import io.github.mscheong01.keyword.krotodc.protobufjavaescapedfieldmessage.toProto 26 | import io.github.mscheong01.keyword.krotodc.protobufjavaescapedmapmessage.toDataClass 27 | import io.github.mscheong01.keyword.krotodc.protobufjavaescapedmapmessage.toProto 28 | import io.github.mscheong01.keyword.krotodc.protobufjavaescapedoneoffieldmessage.toDataClass 29 | import io.github.mscheong01.keyword.krotodc.protobufjavaescapedoneoffieldmessage.toProto 30 | import io.github.mscheong01.keyword.krotodc.protobufjavaescapedoneofmessage.toDataClass 31 | import io.github.mscheong01.keyword.krotodc.protobufjavaescapedoneofmessage.toProto 32 | import io.github.mscheong01.keyword.krotodc.protobufjavaescapedrepeatedmessage.toDataClass 33 | import io.github.mscheong01.keyword.krotodc.protobufjavaescapedrepeatedmessage.toProto 34 | import org.assertj.core.api.Assertions 35 | import org.junit.jupiter.api.Test 36 | 37 | class KeywordEscapeTest { 38 | 39 | /** 40 | * message KeywordMessage { 41 | * string in = 1; 42 | * string fun = 2; 43 | * string if = 3; 44 | * string object = 4; 45 | * oneof as { 46 | * string typeof = 6; 47 | * string while = 7; 48 | * } 49 | * repeated string for = 8; 50 | * map else = 9; 51 | * string public = 10; 52 | * string package = 11; 53 | * } 54 | */ 55 | @Test 56 | fun someKotlinKeywordEscape() { 57 | val proto1 = KeywordMessage.newBuilder() 58 | .setIn("in") 59 | .setFun("fun") 60 | .setIf("if") 61 | .setObject("object") 62 | .setTypeof("typeof") 63 | .addAllFor(listOf("for1", "for2")) 64 | .putAllElse(mapOf("else1" to "else2")) 65 | .setPublic("public") 66 | .setPackage("package") 67 | .build() 68 | val dataClass = proto1.toDataClass() 69 | val proto2 = dataClass.toProto() 70 | Assertions.assertThat(proto1).isEqualTo(proto2) 71 | } 72 | 73 | /** 74 | * message ProtobufJavaEscapedFieldMessage { 75 | * string class = 1; 76 | * } 77 | */ 78 | @Test 79 | fun ProtobufJavaEscapedFieldTest() { 80 | val proto1 = ProtobufJavaEscapedFieldMessage.newBuilder() 81 | .setClass_("class") 82 | .build() 83 | val dataClass = proto1.toDataClass() 84 | val proto2 = dataClass.toProto() 85 | Assertions.assertThat(proto1).isEqualTo(proto2) 86 | } 87 | 88 | /** 89 | * message ProtobufJavaEscapedRepeatedMessage { 90 | * repeated string class = 1; 91 | * } 92 | */ 93 | @Test 94 | fun ProtobufJavaEscapedRepeatedTest() { 95 | val proto1 = ProtobufJavaEscapedRepeatedMessage.newBuilder() 96 | .addClass_("class1") 97 | .addClass_("class2") 98 | .addAllClass_(listOf("class3", "class4")) 99 | .build() 100 | val dataClass = proto1.toDataClass() 101 | val proto2 = dataClass.toProto() 102 | Assertions.assertThat(proto1).isEqualTo(proto2) 103 | } 104 | 105 | /** 106 | * message ProtobufJavaEscapedMapMessage { 107 | * map class = 1; 108 | * } 109 | */ 110 | @Test 111 | fun ProtobufJavaEscapedMapTest() { 112 | val proto1 = ProtobufJavaEscapedMapMessage.newBuilder() 113 | .putClass_("class1", "class2") 114 | .putClass_("class3", "class4") 115 | .putAllClass_(mapOf("class5" to "class6")) 116 | .build() 117 | val dataClass = proto1.toDataClass() 118 | val proto2 = dataClass.toProto() 119 | Assertions.assertThat(proto1).isEqualTo(proto2) 120 | } 121 | 122 | /** 123 | * message ProtobufJavaEscapedOneOfMessage { 124 | * oneof class { 125 | * string name = 1; 126 | * } 127 | * } 128 | */ 129 | @Test 130 | fun ProtobufJavaEscapedOneOfTest() { 131 | val proto1 = ProtobufJavaEscapedOneOfMessage.newBuilder() 132 | .setName("name") 133 | .build() 134 | val dataClass = proto1.toDataClass() 135 | val proto2 = dataClass.toProto() 136 | Assertions.assertThat(proto1).isEqualTo(proto2) 137 | } 138 | 139 | /** 140 | * message ProtobufJavaEscapedOneOfFieldMessage { 141 | * oneof name { 142 | * string class = 1; 143 | * } 144 | * } 145 | */ 146 | @Test 147 | fun ProtobufJavaEscapedOneOfFieldTest() { 148 | val proto1 = ProtobufJavaEscapedOneOfFieldMessage.newBuilder() 149 | .setClass_("class") 150 | .build() 151 | val dataClass = proto1.toDataClass() 152 | val proto2 = dataClass.toProto() 153 | Assertions.assertThat(proto1).isEqualTo(proto2) 154 | } 155 | } 156 | -------------------------------------------------------------------------------- /generator/README.md: -------------------------------------------------------------------------------- 1 | # Generated Code Spec 2 | ### DataClasses 3 | For all protobuf messages including nested types, the plugin will generate a Kotlin data class with the same name. 4 | 5 | ex) 6 | ```protobuf 7 | message Person { 8 | string name = 1; 9 | int32 age = 2; 10 | } 11 | ``` 12 | will be generated as: 13 | ```kotlin 14 | @KrotoDC(forProto = io.github.mscheong01.test.Person::class) 15 | public data class Person( 16 | public val name: String = "", 17 | public val age: Int = 0, 18 | ) 19 | ``` 20 | 21 | **note that enum types will not be regenerated. 22 | 23 | The generated data classes will satisfy these conditions to make them easier to use: 24 | #### Nullability 25 | - fields marked with the `optional` keyword will be nullable. 26 | 27 | ex) 28 | ```protobuf 29 | message OptionalMessage { 30 | optional string optional_string = 1; 31 | optional int32 optional_int = 2; 32 | optional Person optional_person = 3; 33 | } 34 | ``` 35 | will be generated as: 36 | ```kotlin 37 | @KrotoDC(forProto = io.github.mscheong01.test.OptionalMessage::class) 38 | public data class OptionalMessage( 39 | public val optionalString: String? = null, 40 | public val optionalInt: Int? = null, 41 | public val optionalPerson: Person? = null, 42 | ) 43 | ``` 44 | 45 | #### Sealed OneOfs 46 | - protobuf oneofs will be generated as a sealed interface with the same name as the oneof. 47 | - the generated sealed interface will include implementing data classes that has a single field of the type of the contained field. 48 | 49 | ex) 50 | ```protobuf 51 | message OneOfMessage { 52 | oneof oneof_field { 53 | string oneof_string = 1; 54 | int32 oneof_int = 2; 55 | Person oneof_person = 3; 56 | } 57 | } 58 | ``` 59 | will be generated as: 60 | ```kotlin 61 | @KrotoDC(forProto = io.github.mscheong01.test.OneOfMessage::class) 62 | public data class OneOfMessage( 63 | public val oneofField: OneofField? = null, 64 | ) { 65 | public sealed interface OneofField { 66 | public data class OneofString( 67 | public val oneofString: String = "", 68 | ) : OneofField 69 | 70 | public data class OneofInt( 71 | public val oneofInt: Int = 0, 72 | ) : OneofField 73 | 74 | public data class OneofPerson( 75 | public val oneofPerson: Person = io.github.mscheong01.test.krotodc.Person(), 76 | ) : OneofField 77 | } 78 | } 79 | ``` 80 | 81 | #### Predefined Types 82 | - in protobuf-java, there are some predefined types that are not generated as protobuf messages. instead, their java classes are pre-defined in the protobuf-java library. 83 | - for these types, krotoDC will not generate data class and converters by default. this is because: 84 | 1. If we generate data classes for them by this plugin, too much unused excessive code will be included in your resulting package 85 | 2. We could instead consider pre-generate them and include them in the krotoDC-core library. However, we will not do this because: 86 | 1. some pre defined messages, like `google.protobuf.Any`, provides other utility methods like packing which will be lost if we just generate data classes for them 87 | 2. if new predefined types are added to protobuf-java in the future, the generated output will break for these types until we update the krotoDC-core library 88 | - what we instead do is provide a way to customize how to `handle` these types. In [HandledPreDefineType.kt](https://github.com/mscheong01/krotoDC/blob/main/generator/src/main/kotlin/com/github/mscheong01/krotodc/predefinedtypes/HandledPreDefinedType.kt), 89 | we define a list of predefined types that we want to handle. For each type, we define a corresponding enum entry that includes the message descriptor, 90 | the to-be-converted data classes, and templates that specifies how the predefined protobuf-java type should be converted from and to the data class. 91 | adding this definition will allow the plugin to use that information to generate converters for the predefined field type. 92 | - because this behavior is breaking change for that particular predefined type when added, we will include them in future release notes. 93 | 94 | currently, handled pre defined types are as folows: 95 | - `google.protobuf.Timestamp` is converted to `java.time.LocalDateTime` 96 | - `google.protobuf.Duration` is converted to `java.time.Duration` 97 | - `google.protobuf.Empty` is converted to `kotlin.Unit` 98 | - well known wrapper types (e.g. `google.protobuf.StringValue`) are converted to their corresponding primitive *nullable* types (e.g. `kotlin.String?`) 99 | 100 | if you want to add more predefined types to be handled, you can send a PR to add them to the `HandledPreDefinedType` enum class or propose it by subitting an issue. 101 | 102 | ### Converters 103 | fo all generated krotoDC protobuf dataclasses, two converter extension methods will be generated: 104 | - toProto(): the dataclass will be converted to a protobuf message 105 | - toDataClass(): the protobuf message will be converted to a dataclass 106 | for nullable or oneof fields, the converters will contain appropriate checks on protobuf classes (such as `has~`or checking the `~Case` enum) to convert to the appropriate dataclass field type. 107 | 108 | ex) generate `OptionalMessage` converters: 109 | 110 | PersonConverters.kt 111 | ```kotlin 112 | /** 113 | * Converts [Person] to [io.github.mscheong01.test.krotodc.Person] 114 | */ 115 | @KrotoDCConverter( 116 | from = Person::class, 117 | to = io.github.mscheong01.test.krotodc.Person::class, 118 | ) 119 | public fun Person.toDataClass(): io.github.mscheong01.test.krotodc.Person = 120 | io.github.mscheong01.test.krotodc.Person(name = name, 121 | age = age, 122 | ) 123 | 124 | /** 125 | * Converts [io.github.mscheong01.test.krotodc.Person] to [Person] 126 | */ 127 | @KrotoDCConverter( 128 | from = io.github.mscheong01.test.krotodc.Person::class, 129 | to = Person::class, 130 | ) 131 | public fun io.github.mscheong01.test.krotodc.Person.toProto(): Person = Person.newBuilder() 132 | .apply { 133 | setName(this@toProto.name) 134 | setAge(this@toProto.age) 135 | } 136 | .build() 137 | ``` 138 | 139 | these extension functions can be used to convert krotoDC dataclasses to protobuf-java GeneratedMessageV3 classes and vice versa 140 | this is useful for interoperability with other libraries that use protobuf-java 141 | ### ServiceBases & Stubs 142 | for all protobuf services, the plugin will generate a Kotlin Class with a ~GrpcKroto suffix. 143 | the class will contain a CoroutineServiceBase and a CoroutineStub. 144 | Their functionality is identical to that of the grpc-kotlin library, but the method stubs use krotoDC generated protobuf dataclasses as request and response types 145 | 146 | example usage: 147 | server 148 | ```kotlin 149 | // define 150 | class SimpleServiceImpl : SimpleServiceGrpcKroto.SimpleServiceCoroutineImplBase() { 151 | override suspend fun sayHello(request: HelloRequest): HelloResponse { 152 | return HelloResponse( 153 | greeting = "Hello, ${request.name}!" 154 | ) 155 | } 156 | } 157 | // host 158 | val server = ServerBuilder 159 | .forPort(8080) 160 | .addService(SimpleServiceImpl()) 161 | .build() 162 | server.start() 163 | ``` 164 | client 165 | ```kotlin 166 | val channel = ManagedChannelBuilder.forAddress("localhost", 8080).usePlaintext().build() 167 | val stub = SimpleServiceGrpcKroto.SimpleServiceCoroutineStub(channel) 168 | val response = stub.sayHello( 169 | HelloRequest(name = "KrotoDC") 170 | ) 171 | ``` 172 | -------------------------------------------------------------------------------- /generator/src/main/kotlin/io/github/mscheong01/krotodc/predefinedtypes/HandledPreDefinedType.kt: -------------------------------------------------------------------------------- 1 | // Copyright 2023 Minsoo Cheong 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 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 | package io.github.mscheong01.krotodc.predefinedtypes 15 | 16 | import com.google.protobuf.Descriptors.Descriptor 17 | import com.squareup.kotlinpoet.BOOLEAN 18 | import com.squareup.kotlinpoet.DOUBLE 19 | import com.squareup.kotlinpoet.FLOAT 20 | import com.squareup.kotlinpoet.INT 21 | import com.squareup.kotlinpoet.LONG 22 | import com.squareup.kotlinpoet.STRING 23 | import com.squareup.kotlinpoet.TypeName 24 | import com.squareup.kotlinpoet.asTypeName 25 | import io.github.mscheong01.krotodc.import.CodeWithImports 26 | import io.github.mscheong01.krotodc.import.Import 27 | import io.github.mscheong01.krotodc.template.TransformTemplateWithImports 28 | import java.time.LocalDateTime 29 | 30 | enum class HandledPreDefinedType( 31 | val descriptor: Descriptor, 32 | val kotlinType: TypeName, 33 | val defaultValue: CodeWithImports, 34 | val toDataClassTemplate: TransformTemplateWithImports = TransformTemplateWithImports.of("%L"), 35 | val toProtoTransformTemplate: TransformTemplateWithImports = TransformTemplateWithImports.of("%L") 36 | ) { 37 | EMPTY( 38 | com.google.protobuf.Empty.getDescriptor(), 39 | Unit::class.asTypeName(), 40 | CodeWithImports.of("Unit"), 41 | TransformTemplateWithImports.of("Unit"), 42 | TransformTemplateWithImports.of( 43 | "Empty.getDefaultInstance()", 44 | setOf(Import("com.google.protobuf", listOf("Empty"))) 45 | ) 46 | ), 47 | 48 | // time types 49 | TIMESTAMP( 50 | com.google.protobuf.Timestamp.getDescriptor(), 51 | LocalDateTime::class.asTypeName(), 52 | CodeWithImports.of( 53 | "Timestamp.getDefaultInstance().toLocalDateTime()", 54 | setOf( 55 | Import("com.google.protobuf", listOf("Timestamp")), 56 | Import("io.github.mscheong01.krotodc", listOf("toLocalDateTime")) 57 | ) 58 | ), 59 | TransformTemplateWithImports.of( 60 | "%L.toLocalDateTime()", 61 | setOf(Import("io.github.mscheong01.krotodc", listOf("toLocalDateTime"))) 62 | ), 63 | TransformTemplateWithImports.of( 64 | "%L.toProtoTimestamp()", 65 | setOf(Import("io.github.mscheong01.krotodc", listOf("toProtoTimestamp"))) 66 | ) 67 | ), 68 | DURATION( 69 | com.google.protobuf.Duration.getDescriptor(), 70 | java.time.Duration::class.asTypeName(), 71 | CodeWithImports.of("java.time.Duration.ZERO"), 72 | TransformTemplateWithImports.of( 73 | "%L.toDuration()", 74 | setOf(Import("io.github.mscheong01.krotodc", listOf("toDuration"))) 75 | ), 76 | TransformTemplateWithImports.of( 77 | "%L.toProtoDuration()", 78 | setOf(Import("io.github.mscheong01.krotodc", listOf("toProtoDuration"))) 79 | ) 80 | ), 81 | 82 | // well known wrapper types 83 | DOUBLE_VALUE( 84 | com.google.protobuf.DoubleValue.getDescriptor(), 85 | DOUBLE.copy(nullable = true), 86 | CodeWithImports.of("null"), 87 | TransformTemplateWithImports.of("%L.value"), 88 | TransformTemplateWithImports.of( 89 | "%L.toDoubleValue()", 90 | setOf(Import("io.github.mscheong01.krotodc", listOf("toDoubleValue"))) 91 | ) 92 | ), 93 | FLOAT_VALUE( 94 | com.google.protobuf.FloatValue.getDescriptor(), 95 | FLOAT.copy(nullable = true), 96 | CodeWithImports.of("null"), 97 | TransformTemplateWithImports.of("%L.value"), 98 | TransformTemplateWithImports.of( 99 | "%L.toFloatValue()", 100 | setOf(Import("io.github.mscheong01.krotodc", listOf("toFloatValue"))) 101 | ) 102 | ), 103 | INT64_VALUE( 104 | com.google.protobuf.Int64Value.getDescriptor(), 105 | LONG.copy(nullable = true), 106 | CodeWithImports.of("null"), 107 | TransformTemplateWithImports.of("%L.value"), 108 | TransformTemplateWithImports.of( 109 | "%L.toInt64Value()", 110 | setOf(Import("io.github.mscheong01.krotodc", listOf("toInt64Value"))) 111 | ) 112 | ), 113 | UINT64_VALUE( 114 | com.google.protobuf.UInt64Value.getDescriptor(), 115 | LONG.copy(nullable = true), 116 | CodeWithImports.of("null"), 117 | TransformTemplateWithImports.of("%L.value"), 118 | TransformTemplateWithImports.of( 119 | "%L.toUInt64Value()", 120 | setOf(Import("io.github.mscheong01.krotodc", listOf("toUInt64Value"))) 121 | ) 122 | ), 123 | INT32_VALUE( 124 | com.google.protobuf.Int32Value.getDescriptor(), 125 | INT.copy(nullable = true), 126 | CodeWithImports.of("null"), 127 | TransformTemplateWithImports.of("%L.value"), 128 | TransformTemplateWithImports.of( 129 | "%L.toInt32Value()", 130 | setOf(Import("io.github.mscheong01.krotodc", listOf("toInt32Value"))) 131 | ) 132 | ), 133 | UINT32_VALUE( 134 | com.google.protobuf.UInt32Value.getDescriptor(), 135 | INT.copy(nullable = true), 136 | CodeWithImports.of("null"), 137 | TransformTemplateWithImports.of("%L.value"), 138 | TransformTemplateWithImports.of( 139 | "%L.toUInt32Value()", 140 | setOf(Import("io.github.mscheong01.krotodc", listOf("toUInt32Value"))) 141 | ) 142 | ), 143 | BOOL_VALUE( 144 | com.google.protobuf.BoolValue.getDescriptor(), 145 | BOOLEAN.copy(nullable = true), 146 | CodeWithImports.of("null"), 147 | TransformTemplateWithImports.of("%L.value"), 148 | TransformTemplateWithImports.of( 149 | "%L.toBoolValue()", 150 | setOf(Import("io.github.mscheong01.krotodc", listOf("toBoolValue"))) 151 | ) 152 | ), 153 | STRING_VALUE( 154 | com.google.protobuf.StringValue.getDescriptor(), 155 | STRING.copy(nullable = true), 156 | CodeWithImports.of("null"), 157 | TransformTemplateWithImports.of("%L.value"), 158 | TransformTemplateWithImports.of( 159 | "%L.toStringValue()", 160 | setOf(Import("io.github.mscheong01.krotodc", listOf("toStringValue"))) 161 | ) 162 | ), 163 | BYTES_VALUE( 164 | com.google.protobuf.BytesValue.getDescriptor(), 165 | com.google.protobuf.ByteString::class.asTypeName().copy(nullable = true), 166 | CodeWithImports.of("null"), 167 | TransformTemplateWithImports.of("%L.value"), 168 | TransformTemplateWithImports.of( 169 | "%L.toBytesValue()", 170 | setOf(Import("io.github.mscheong01.krotodc", listOf("toBytesValue"))) 171 | ) 172 | ) 173 | ; 174 | 175 | companion object { 176 | fun valueOfByDescriptor(descriptor: Descriptor): HandledPreDefinedType { 177 | return values().first { it.descriptor.fullName == descriptor.fullName } 178 | } 179 | } 180 | } 181 | -------------------------------------------------------------------------------- /generator/src/main/kotlin/io/github/mscheong01/krotodc/util/DescriptorExtensions.kt: -------------------------------------------------------------------------------- 1 | // Copyright 2023 Minsoo Cheong 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 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 | package io.github.mscheong01.krotodc.util 15 | 16 | import com.google.protobuf.Descriptors 17 | import com.google.protobuf.Descriptors.Descriptor 18 | import com.google.protobuf.Descriptors.EnumDescriptor 19 | import com.google.protobuf.Descriptors.FieldDescriptor 20 | import com.google.protobuf.Descriptors.FileDescriptor 21 | import com.squareup.kotlinpoet.ClassName 22 | import com.squareup.kotlinpoet.CodeBlock 23 | import io.github.mscheong01.krotodc.import.Import 24 | import io.github.mscheong01.krotodc.predefinedtypes.HandledPreDefinedType 25 | import java.lang.reflect.Method 26 | 27 | val hasOptionalKeywordMethod: Method = FieldDescriptor::class.java 28 | .getDeclaredMethod("hasOptionalKeyword") 29 | .apply { 30 | isAccessible = true 31 | } 32 | 33 | val FileDescriptor.javaPackage: String 34 | get() = when (val javaPackage = this.options.javaPackage) { 35 | "" -> this.`package` 36 | else -> javaPackage 37 | } 38 | 39 | val FileDescriptor.krotoDCPackage: String 40 | get() = this.javaPackage + ".krotodc" 41 | 42 | val FileDescriptor.simpleName: String 43 | get() = this.name.split('/').last().split('.').first() 44 | 45 | val Descriptor.protobufJavaTypeName: ClassName 46 | get() { 47 | var simpleNames = this.fullName 48 | .replace(this.file.`package` + ".", "") 49 | .split('.') 50 | if (!this.file.options.javaMultipleFiles) { 51 | simpleNames = listOf( 52 | if (this.file.options.hasJavaOuterClassname()) { 53 | this.file.options.javaOuterClassname 54 | } else { 55 | fieldNameToJsonName(this.file.simpleName).capitalize() 56 | } 57 | ) + simpleNames 58 | } 59 | return ClassName(this.file.javaPackage, simpleNames) 60 | } 61 | 62 | val Descriptor.krotoDCTypeName: ClassName 63 | get() { 64 | return ClassName(this.file.krotoDCPackage, simpleNames) 65 | } 66 | 67 | val Descriptor.simpleNames: List 68 | get() = this.fullName 69 | .replace(this.file.`package` + ".", "") 70 | .split('.') 71 | 72 | fun Descriptor.isPredefinedType(): Boolean { 73 | return this.file.name.startsWith("google/") 74 | } 75 | 76 | fun Descriptor.isWellKnownWrapperType(): Boolean { 77 | return when (this.fullName) { 78 | in listOf( 79 | HandledPreDefinedType.DOUBLE_VALUE.descriptor.fullName, 80 | HandledPreDefinedType.FLOAT_VALUE.descriptor.fullName, 81 | HandledPreDefinedType.INT32_VALUE.descriptor.fullName, 82 | HandledPreDefinedType.INT64_VALUE.descriptor.fullName, 83 | HandledPreDefinedType.UINT32_VALUE.descriptor.fullName, 84 | HandledPreDefinedType.UINT64_VALUE.descriptor.fullName, 85 | HandledPreDefinedType.BOOL_VALUE.descriptor.fullName, 86 | HandledPreDefinedType.STRING_VALUE.descriptor.fullName, 87 | HandledPreDefinedType.BYTES_VALUE.descriptor.fullName 88 | ) -> true 89 | else -> false 90 | } 91 | } 92 | 93 | fun Descriptor.isHandledPreDefinedType(): Boolean { 94 | return HandledPreDefinedType.values().any { 95 | it.descriptor.fullName == this.fullName 96 | } 97 | } 98 | 99 | val EnumDescriptor.protobufJavaTypeName: ClassName 100 | get() { 101 | var simpleNames = this.fullName 102 | .replace(this.file.`package` + ".", "") 103 | .split('.') 104 | if (!this.file.options.javaMultipleFiles) { 105 | simpleNames = listOf( 106 | if (this.file.options.hasJavaOuterClassname()) { 107 | this.file.options.javaOuterClassname 108 | } else { 109 | fieldNameToJsonName(this.file.simpleName).capitalize() 110 | } 111 | ) + simpleNames 112 | } 113 | return ClassName(this.file.javaPackage, simpleNames) 114 | } 115 | 116 | val EnumDescriptor.krotoDCTypeName: ClassName 117 | get() { 118 | val simpleNames = this.fullName 119 | .replace(this.file.`package` + ".", "") 120 | .split('.') 121 | return ClassName(this.file.krotoDCPackage, simpleNames) 122 | } 123 | 124 | val FieldDescriptor.isKrotoDCOptional: Boolean 125 | get() { 126 | if (hasOptionalKeywordMethod.invoke(this) as Boolean) { 127 | return true 128 | } 129 | if (this.isRepeated || isMapField) { 130 | return false 131 | } 132 | if (this.type != FieldDescriptor.Type.MESSAGE) { 133 | return false 134 | } 135 | return this.messageType.isWellKnownWrapperType() 136 | } 137 | 138 | val Descriptors.ServiceDescriptor.krotoServiceImplBaseName: ClassName 139 | get() { 140 | return ClassName(this.file.krotoDCPackage, "${this.name}CoroutineImplBase") 141 | } 142 | 143 | val Descriptors.ServiceDescriptor.krotoClientStubName: ClassName 144 | get() { 145 | return ClassName(this.file.krotoDCPackage, "${this.name}CoroutineStub") 146 | } 147 | 148 | val Descriptors.ServiceDescriptor.grpcClass: ClassName 149 | get() = 150 | ClassName(this.file.javaPackage, this.name + GRPC_JAVA_CLASS_NAME_SUFFIX) 151 | 152 | val Descriptors.MethodDescriptor.descriptorCode: CodeBlock 153 | get() = CodeBlock.of( 154 | "%T.%L()", 155 | service.grpcClass, 156 | "get" + this.name.capitalize() + "Method" 157 | ) 158 | 159 | val Descriptor.converterImports: Set 160 | get() { 161 | if ( 162 | this.isPredefinedType() || this.options.mapEntry 163 | ) { 164 | return setOf() 165 | } 166 | return setOf( 167 | this.toProtoImport, 168 | this.toDataClassImport 169 | ) 170 | } 171 | 172 | val Descriptor.toProtoImport: Import 173 | get() { 174 | return Import( 175 | this.file.krotoDCPackage + '.' + this.simpleNames.first().lowercase(), 176 | listOf("toProto") 177 | ) 178 | } 179 | val Descriptor.toDataClassImport: Import 180 | get() { 181 | return Import( 182 | this.file.krotoDCPackage + '.' + this.simpleNames.first().lowercase(), 183 | listOf("toDataClass") 184 | ) 185 | } 186 | 187 | /** 188 | * beware: does not escape Kotlin keywords 189 | */ 190 | val FieldDescriptor.javaFieldName: String 191 | get() { 192 | val jsonName = this.jsonName 193 | /** 194 | * protobuf-java escapes special fields in order to avoid name clashes with Java/Protobuf keywords 195 | * @see https://github.com/protocolbuffers/protobuf/blob/2cf94fafe39eeab44d3ab83898aabf03ff930d7a/java/core/src/main/java/com/google/protobuf/DescriptorMessageInfoFactory.java#L629C1-L648 196 | */ 197 | return if (PROTOBUF_JAVA_SPECIAL_FIELD_NAMES.contains(jsonName.capitalize())) { 198 | jsonName + "_" 199 | } else { 200 | jsonName 201 | } 202 | } 203 | 204 | /** 205 | * @see https://github.com/protocolbuffers/protobuf/blob/2cf94fafe39eeab44d3ab83898aabf03ff930d7a/java/core/src/main/java/com/google/protobuf/DescriptorMessageInfoFactory.java#L72 206 | */ 207 | val PROTOBUF_JAVA_SPECIAL_FIELD_NAMES = setOf( 208 | // java.lang.Object: 209 | "Class", 210 | // com.google.protobuf.MessageLiteOrBuilder: 211 | "DefaultInstanceForType", 212 | // com.google.protobuf.MessageLite: 213 | "ParserForType", 214 | "SerializedSize", 215 | // com.google.protobuf.MessageOrBuilder: 216 | "AllFields", 217 | "DescriptorForType", 218 | "InitializationErrorString", 219 | "UnknownFields", 220 | // obsolete. kept for backwards compatibility of generated code 221 | "CachedSize" 222 | ) 223 | -------------------------------------------------------------------------------- /generator/src/main/kotlin/io/github/mscheong01/krotodc/specgenerators/function/MessageToProtoFunctionGenerator.kt: -------------------------------------------------------------------------------- 1 | // Copyright 2023 Minsoo Cheong 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 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 | package io.github.mscheong01.krotodc.specgenerators.function 15 | 16 | import com.google.protobuf.Descriptors.Descriptor 17 | import com.google.protobuf.Descriptors.FieldDescriptor 18 | import com.squareup.kotlinpoet.AnnotationSpec 19 | import com.squareup.kotlinpoet.ClassName 20 | import com.squareup.kotlinpoet.CodeBlock 21 | import com.squareup.kotlinpoet.FunSpec 22 | import io.github.mscheong01.krotodc.KrotoDCConverter 23 | import io.github.mscheong01.krotodc.import.CodeWithImports 24 | import io.github.mscheong01.krotodc.import.FunSpecsWithImports 25 | import io.github.mscheong01.krotodc.import.Import 26 | import io.github.mscheong01.krotodc.predefinedtypes.HandledPreDefinedType 27 | import io.github.mscheong01.krotodc.specgenerators.FunSpecGenerator 28 | import io.github.mscheong01.krotodc.template.TransformTemplateWithImports 29 | import io.github.mscheong01.krotodc.util.MAP_ENTRY_VALUE_FIELD_NUMBER 30 | import io.github.mscheong01.krotodc.util.capitalize 31 | import io.github.mscheong01.krotodc.util.escapeIfNecessary 32 | import io.github.mscheong01.krotodc.util.fieldNameToJsonName 33 | import io.github.mscheong01.krotodc.util.isHandledPreDefinedType 34 | import io.github.mscheong01.krotodc.util.isKrotoDCOptional 35 | import io.github.mscheong01.krotodc.util.isPredefinedType 36 | import io.github.mscheong01.krotodc.util.javaFieldName 37 | import io.github.mscheong01.krotodc.util.krotoDCPackage 38 | import io.github.mscheong01.krotodc.util.krotoDCTypeName 39 | import io.github.mscheong01.krotodc.util.protobufJavaTypeName 40 | import io.github.mscheong01.krotodc.util.simpleNames 41 | import io.github.mscheong01.krotodc.util.toProtoImport 42 | 43 | class MessageToProtoFunctionGenerator : FunSpecGenerator { 44 | override fun generate(descriptor: Descriptor): FunSpecsWithImports { 45 | val imports = mutableSetOf() 46 | val generatedType = descriptor.krotoDCTypeName 47 | val protoType = descriptor.protobufJavaTypeName 48 | val functionBuilder = FunSpec.builder("toProto") 49 | .receiver(generatedType) 50 | .returns(protoType) 51 | 52 | functionBuilder.addCode("return %T.newBuilder()\n", protoType) 53 | 54 | functionBuilder.beginControlFlow(".apply") 55 | 56 | for (oneOf in descriptor.realOneofs) { 57 | val oneOfJsonName = fieldNameToJsonName(oneOf.name) 58 | functionBuilder.beginControlFlow("when (%L)", oneOfJsonName.escapeIfNecessary()) 59 | for (field in oneOf.fields) { 60 | val oneOfFieldDataClassName = ClassName( 61 | oneOf.file.krotoDCPackage, 62 | *descriptor.simpleNames.toMutableList().apply { 63 | add(oneOfJsonName.capitalize()) 64 | add(field.jsonName.capitalize()) 65 | }.toTypedArray() 66 | ) 67 | functionBuilder.beginControlFlow("is %L ->", oneOfFieldDataClassName) 68 | val (template, downStreamImports) = transformCodeTemplate(field) 69 | functionBuilder.addStatement( 70 | "set${field.javaFieldName.capitalize()}(%L)", 71 | CodeBlock.of( 72 | "%L", 73 | template.safeCall( 74 | CodeBlock.of( 75 | "%L.%L", 76 | oneOfJsonName.escapeIfNecessary(), 77 | field.jsonName.escapeIfNecessary() 78 | ) 79 | ) 80 | ) 81 | ) 82 | 83 | functionBuilder.endControlFlow() 84 | imports.addAll(downStreamImports) 85 | } 86 | functionBuilder.addStatement("null -> {}") 87 | functionBuilder.endControlFlow() 88 | } 89 | 90 | for (field in descriptor.fields) { 91 | if (field.name in descriptor.realOneofs.map { it.fields }.flatten().map { it.name }.toSet()) { 92 | continue 93 | } 94 | val fieldName = "this@toProto.${field.jsonName.escapeIfNecessary()}" 95 | val optional = field.isKrotoDCOptional 96 | if (optional) { 97 | functionBuilder.beginControlFlow("if ($fieldName != null)") 98 | } 99 | 100 | val codeWithImports = if (field.isMapField) { 101 | val valueField = field.messageType.findFieldByNumber(MAP_ENTRY_VALUE_FIELD_NUMBER) 102 | val (template, downStreamImports) = transformCodeTemplate(valueField) 103 | val mapCodeBlock = if (template.value == "%L") { 104 | CodeBlock.of("%L", fieldName) 105 | } else { 106 | CodeBlock.of("%L.mapValues { %L }", fieldName, template.safeCall("it.value")) 107 | } 108 | CodeWithImports.of(mapCodeBlock, downStreamImports) 109 | } else if (field.isRepeated) { 110 | val (template, downStreamImports) = transformCodeTemplate(field) 111 | val repeatedCodeBlock = if (template.value == "%L") { 112 | CodeBlock.of("%L", fieldName) 113 | } else { 114 | CodeBlock.of("%L.map { %L }", fieldName, template.safeCall("it")) 115 | } 116 | CodeWithImports.of(repeatedCodeBlock, downStreamImports) 117 | } else { 118 | val (template, downStreamImports) = transformCodeTemplate(field) 119 | CodeWithImports.of( 120 | template.safeCall(fieldName), 121 | downStreamImports 122 | ) 123 | } 124 | val accessorMethodName = when { 125 | field.isMapField -> "putAll${field.javaFieldName.capitalize()}" 126 | field.isRepeated -> "addAll${field.javaFieldName.capitalize()}" 127 | else -> "set${field.javaFieldName.capitalize()}" 128 | } 129 | imports.addAll(codeWithImports.imports) 130 | functionBuilder.addCode( 131 | CodeBlock.of( 132 | "$accessorMethodName(%L)\n", 133 | codeWithImports.code 134 | ) 135 | ) 136 | if (optional) { 137 | functionBuilder.endControlFlow() 138 | } 139 | } 140 | functionBuilder.endControlFlow() 141 | 142 | functionBuilder.addCode(".build()") 143 | functionBuilder.addKdoc( 144 | "Converts [%T] to [%T]", 145 | generatedType, 146 | protoType 147 | ) 148 | functionBuilder.addAnnotation( 149 | AnnotationSpec.Companion.builder(KrotoDCConverter::class) 150 | .addMember("from = %T::class", generatedType) 151 | .addMember("to = %T::class", protoType) 152 | .build() 153 | ) 154 | 155 | return FunSpecsWithImports( 156 | listOf(functionBuilder.build()), 157 | imports 158 | ) 159 | } 160 | 161 | fun transformCodeTemplate(field: FieldDescriptor): TransformTemplateWithImports { 162 | return when (field.type) { 163 | FieldDescriptor.Type.MESSAGE -> { 164 | messageTypeTransformCodeTemplate(field.messageType) 165 | } 166 | else -> TransformTemplateWithImports.of("%L") 167 | } 168 | } 169 | 170 | companion object { 171 | fun messageTypeTransformCodeTemplate( 172 | descriptor: Descriptor 173 | ): TransformTemplateWithImports { 174 | return if (descriptor.isPredefinedType()) { 175 | preDefinedTypeTransformCodeTemplate(descriptor) 176 | } else { 177 | TransformTemplateWithImports.of("%L.toProto()", setOf(descriptor.toProtoImport)) 178 | } 179 | } 180 | 181 | private fun preDefinedTypeTransformCodeTemplate( 182 | descriptor: Descriptor 183 | ): TransformTemplateWithImports { 184 | return if (descriptor.isHandledPreDefinedType()) { 185 | HandledPreDefinedType.valueOfByDescriptor(descriptor).toProtoTransformTemplate 186 | } else { 187 | TransformTemplateWithImports.of("%L") 188 | } 189 | } 190 | } 191 | } 192 | -------------------------------------------------------------------------------- /gradlew: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | # 4 | # Copyright © 2015-2021 the original authors. 5 | # 6 | # Licensed under the Apache License, Version 2.0 (the "License"); 7 | # you may not use this file except in compliance with the License. 8 | # You may obtain a copy of the License at 9 | # 10 | # https://www.apache.org/licenses/LICENSE-2.0 11 | # 12 | # Unless required by applicable law or agreed to in writing, software 13 | # distributed under the License is distributed on an "AS IS" BASIS, 14 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 15 | # See the License for the specific language governing permissions and 16 | # limitations under the License. 17 | # 18 | 19 | ############################################################################## 20 | # 21 | # Gradle start up script for POSIX generated by Gradle. 22 | # 23 | # Important for running: 24 | # 25 | # (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is 26 | # noncompliant, but you have some other compliant shell such as ksh or 27 | # bash, then to run this script, type that shell name before the whole 28 | # command line, like: 29 | # 30 | # ksh Gradle 31 | # 32 | # Busybox and similar reduced shells will NOT work, because this script 33 | # requires all of these POSIX shell features: 34 | # * functions; 35 | # * expansions «$var», «${var}», «${var:-default}», «${var+SET}», 36 | # «${var#prefix}», «${var%suffix}», and «$( cmd )»; 37 | # * compound commands having a testable exit status, especially «case»; 38 | # * various built-in commands including «command», «set», and «ulimit». 39 | # 40 | # Important for patching: 41 | # 42 | # (2) This script targets any POSIX shell, so it avoids extensions provided 43 | # by Bash, Ksh, etc; in particular arrays are avoided. 44 | # 45 | # The "traditional" practice of packing multiple parameters into a 46 | # space-separated string is a well documented source of bugs and security 47 | # problems, so this is (mostly) avoided, by progressively accumulating 48 | # options in "$@", and eventually passing that to Java. 49 | # 50 | # Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS, 51 | # and GRADLE_OPTS) rely on word-splitting, this is performed explicitly; 52 | # see the in-line comments for details. 53 | # 54 | # There are tweaks for specific operating systems such as AIX, CygWin, 55 | # Darwin, MinGW, and NonStop. 56 | # 57 | # (3) This script is generated from the Groovy template 58 | # https://github.com/gradle/gradle/blob/master/subprojects/plugins/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt 59 | # within the Gradle project. 60 | # 61 | # You can find Gradle at https://github.com/gradle/gradle/. 62 | # 63 | ############################################################################## 64 | 65 | # Attempt to set APP_HOME 66 | 67 | # Resolve links: $0 may be a link 68 | app_path=$0 69 | 70 | # Need this for daisy-chained symlinks. 71 | while 72 | APP_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path 73 | [ -h "$app_path" ] 74 | do 75 | ls=$( ls -ld "$app_path" ) 76 | link=${ls#*' -> '} 77 | case $link in #( 78 | /*) app_path=$link ;; #( 79 | *) app_path=$APP_HOME$link ;; 80 | esac 81 | done 82 | 83 | APP_HOME=$( cd "${APP_HOME:-./}" && pwd -P ) || exit 84 | 85 | APP_NAME="Gradle" 86 | APP_BASE_NAME=${0##*/} 87 | 88 | # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 89 | DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' 90 | 91 | # Use the maximum available, or set MAX_FD != -1 to use that value. 92 | MAX_FD=maximum 93 | 94 | warn () { 95 | echo "$*" 96 | } >&2 97 | 98 | die () { 99 | echo 100 | echo "$*" 101 | echo 102 | exit 1 103 | } >&2 104 | 105 | # OS specific support (must be 'true' or 'false'). 106 | cygwin=false 107 | msys=false 108 | darwin=false 109 | nonstop=false 110 | case "$( uname )" in #( 111 | CYGWIN* ) cygwin=true ;; #( 112 | Darwin* ) darwin=true ;; #( 113 | MSYS* | MINGW* ) msys=true ;; #( 114 | NONSTOP* ) nonstop=true ;; 115 | esac 116 | 117 | CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar 118 | 119 | 120 | # Determine the Java command to use to start the JVM. 121 | if [ -n "$JAVA_HOME" ] ; then 122 | if [ -x "$JAVA_HOME/jre/sh/java" ] ; then 123 | # IBM's JDK on AIX uses strange locations for the executables 124 | JAVACMD=$JAVA_HOME/jre/sh/java 125 | else 126 | JAVACMD=$JAVA_HOME/bin/java 127 | fi 128 | if [ ! -x "$JAVACMD" ] ; then 129 | die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME 130 | 131 | Please set the JAVA_HOME variable in your environment to match the 132 | location of your Java installation." 133 | fi 134 | else 135 | JAVACMD=java 136 | which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 137 | 138 | Please set the JAVA_HOME variable in your environment to match the 139 | location of your Java installation." 140 | fi 141 | 142 | # Increase the maximum file descriptors if we can. 143 | if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then 144 | case $MAX_FD in #( 145 | max*) 146 | MAX_FD=$( ulimit -H -n ) || 147 | warn "Could not query maximum file descriptor limit" 148 | esac 149 | case $MAX_FD in #( 150 | '' | soft) :;; #( 151 | *) 152 | ulimit -n "$MAX_FD" || 153 | warn "Could not set maximum file descriptor limit to $MAX_FD" 154 | esac 155 | fi 156 | 157 | # Collect all arguments for the java command, stacking in reverse order: 158 | # * args from the command line 159 | # * the main class name 160 | # * -classpath 161 | # * -D...appname settings 162 | # * --module-path (only if needed) 163 | # * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables. 164 | 165 | # For Cygwin or MSYS, switch paths to Windows format before running java 166 | if "$cygwin" || "$msys" ; then 167 | APP_HOME=$( cygpath --path --mixed "$APP_HOME" ) 168 | CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" ) 169 | 170 | JAVACMD=$( cygpath --unix "$JAVACMD" ) 171 | 172 | # Now convert the arguments - kludge to limit ourselves to /bin/sh 173 | for arg do 174 | if 175 | case $arg in #( 176 | -*) false ;; # don't mess with options #( 177 | /?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath 178 | [ -e "$t" ] ;; #( 179 | *) false ;; 180 | esac 181 | then 182 | arg=$( cygpath --path --ignore --mixed "$arg" ) 183 | fi 184 | # Roll the args list around exactly as many times as the number of 185 | # args, so each arg winds up back in the position where it started, but 186 | # possibly modified. 187 | # 188 | # NB: a `for` loop captures its iteration list before it begins, so 189 | # changing the positional parameters here affects neither the number of 190 | # iterations, nor the values presented in `arg`. 191 | shift # remove old arg 192 | set -- "$@" "$arg" # push replacement arg 193 | done 194 | fi 195 | 196 | # Collect all arguments for the java command; 197 | # * $DEFAULT_JVM_OPTS, $JAVA_OPTS, and $GRADLE_OPTS can contain fragments of 198 | # shell script including quotes and variable substitutions, so put them in 199 | # double quotes to make sure that they get re-expanded; and 200 | # * put everything else in single quotes, so that it's not re-expanded. 201 | 202 | set -- \ 203 | "-Dorg.gradle.appname=$APP_BASE_NAME" \ 204 | -classpath "$CLASSPATH" \ 205 | org.gradle.wrapper.GradleWrapperMain \ 206 | "$@" 207 | 208 | # Stop when "xargs" is not available. 209 | if ! command -v xargs >/dev/null 2>&1 210 | then 211 | die "xargs is not available" 212 | fi 213 | 214 | # Use "xargs" to parse quoted args. 215 | # 216 | # With -n1 it outputs one arg per line, with the quotes and backslashes removed. 217 | # 218 | # In Bash we could simply go: 219 | # 220 | # readarray ARGS < <( xargs -n1 <<<"$var" ) && 221 | # set -- "${ARGS[@]}" "$@" 222 | # 223 | # but POSIX shell has neither arrays nor command substitution, so instead we 224 | # post-process each arg (as a line of input to sed) to backslash-escape any 225 | # character that might be a shell metacharacter, then use eval to reverse 226 | # that process (while maintaining the separation between arguments), and wrap 227 | # the whole thing up as a single "set" statement. 228 | # 229 | # This will of course break if any of these variables contains a newline or 230 | # an unmatched quote. 231 | # 232 | 233 | eval "set -- $( 234 | printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" | 235 | xargs -n1 | 236 | sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' | 237 | tr '\n' ' ' 238 | )" '"$@"' 239 | 240 | exec "$JAVACMD" "$@" 241 | -------------------------------------------------------------------------------- /generator/src/main/kotlin/io/github/mscheong01/krotodc/specgenerators/function/MessageToDataClassFunctionGenerator.kt: -------------------------------------------------------------------------------- 1 | // Copyright 2023 Minsoo Cheong 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 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 | package io.github.mscheong01.krotodc.specgenerators.function 15 | 16 | import com.google.protobuf.Descriptors.Descriptor 17 | import com.google.protobuf.Descriptors.FieldDescriptor 18 | import com.squareup.kotlinpoet.AnnotationSpec 19 | import com.squareup.kotlinpoet.ClassName 20 | import com.squareup.kotlinpoet.CodeBlock 21 | import com.squareup.kotlinpoet.FunSpec 22 | import io.github.mscheong01.krotodc.KrotoDCConverter 23 | import io.github.mscheong01.krotodc.import.CodeWithImports 24 | import io.github.mscheong01.krotodc.import.FunSpecsWithImports 25 | import io.github.mscheong01.krotodc.import.Import 26 | import io.github.mscheong01.krotodc.predefinedtypes.HandledPreDefinedType 27 | import io.github.mscheong01.krotodc.specgenerators.FunSpecGenerator 28 | import io.github.mscheong01.krotodc.template.TransformTemplateWithImports 29 | import io.github.mscheong01.krotodc.util.MAP_ENTRY_VALUE_FIELD_NUMBER 30 | import io.github.mscheong01.krotodc.util.capitalize 31 | import io.github.mscheong01.krotodc.util.endControlFlowWithComma 32 | import io.github.mscheong01.krotodc.util.escapeIfNecessary 33 | import io.github.mscheong01.krotodc.util.fieldNameToJsonName 34 | import io.github.mscheong01.krotodc.util.isHandledPreDefinedType 35 | import io.github.mscheong01.krotodc.util.isKrotoDCOptional 36 | import io.github.mscheong01.krotodc.util.isPredefinedType 37 | import io.github.mscheong01.krotodc.util.javaFieldName 38 | import io.github.mscheong01.krotodc.util.krotoDCPackage 39 | import io.github.mscheong01.krotodc.util.krotoDCTypeName 40 | import io.github.mscheong01.krotodc.util.protobufJavaTypeName 41 | import io.github.mscheong01.krotodc.util.simpleNames 42 | import io.github.mscheong01.krotodc.util.toDataClassImport 43 | 44 | class MessageToDataClassFunctionGenerator : FunSpecGenerator { 45 | override fun generate(descriptor: Descriptor): FunSpecsWithImports { 46 | val imports = mutableSetOf() 47 | val generatedType = descriptor.krotoDCTypeName 48 | val protoType = descriptor.protobufJavaTypeName 49 | val functionBuilder = FunSpec.builder("toDataClass") 50 | .receiver(protoType) 51 | .returns(generatedType) 52 | 53 | functionBuilder.addCode("return %T(", generatedType) 54 | 55 | for (oneOf in descriptor.realOneofs) { 56 | val oneOfJsonName = fieldNameToJsonName(oneOf.name) 57 | functionBuilder.beginControlFlow( 58 | "%L = when (%LCase)", 59 | oneOfJsonName.escapeIfNecessary(), 60 | oneOfJsonName 61 | ) 62 | for (field in oneOf.fields) { 63 | val dataClassFieldName = field.jsonName 64 | val protoFieldName = field.javaFieldName 65 | val (template, downStreamImports) = transformCodeTemplate(field) 66 | val oneOfDataClassName = ClassName( 67 | oneOf.file.krotoDCPackage, 68 | *descriptor.simpleNames.toMutableList().apply { 69 | add(oneOfJsonName.capitalize()) 70 | add(field.jsonName.capitalize()) 71 | }.toTypedArray() 72 | ) 73 | functionBuilder.addStatement( 74 | "%L.%LCase.%L -> %L( %L = %L )".trimIndent(), 75 | protoType, 76 | oneOfJsonName.capitalize(), 77 | field.name.uppercase(), 78 | oneOfDataClassName.canonicalName, 79 | dataClassFieldName.escapeIfNecessary(), 80 | template.safeCall(protoFieldName.escapeIfNecessary()) 81 | ) 82 | imports.addAll(downStreamImports) 83 | } 84 | functionBuilder.addStatement("%L.%LCase.%L -> null", protoType, oneOfJsonName.capitalize(), "${oneOf.name.replace("_", "").uppercase()}_NOT_SET".uppercase()) 85 | functionBuilder.addStatement("null -> null") 86 | functionBuilder.endControlFlowWithComma() 87 | } 88 | 89 | for (field in descriptor.fields) { 90 | if (field.name in descriptor.realOneofs.map { it.fields }.flatten().map { it.name }.toSet()) { 91 | continue 92 | } 93 | 94 | val dataClassFieldName = field.jsonName 95 | val protoFieldName = field.javaFieldName 96 | val optional = field.isKrotoDCOptional 97 | functionBuilder.addCode("%L = ", dataClassFieldName.escapeIfNecessary()) 98 | if (optional) { 99 | functionBuilder.beginControlFlow("if (has${protoFieldName.capitalize()}())") 100 | } 101 | 102 | val codeWithImports = if (field.isMapField) { 103 | val valueField = field.messageType.findFieldByNumber(MAP_ENTRY_VALUE_FIELD_NUMBER) 104 | val (template, downStreamImports) = transformCodeTemplate(valueField) 105 | val mapCodeBlock = if (template.value == "%L") { 106 | CodeBlock.of("%LMap", protoFieldName) 107 | } else { 108 | CodeBlock.of( 109 | "%LMap.mapValues { %L }", 110 | protoFieldName, 111 | template.safeCall("it.value") 112 | ) 113 | } 114 | CodeWithImports.of(mapCodeBlock, downStreamImports) 115 | } else if (field.isRepeated) { 116 | val (template, downStreamImports) = transformCodeTemplate(field) 117 | val repeatedCodeBlock = if (template.value == "%L") { 118 | CodeBlock.of("%LList", protoFieldName) 119 | } else { 120 | CodeBlock.of("%LList.map { %L }", protoFieldName, template.safeCall("it")) 121 | } 122 | CodeWithImports.of(repeatedCodeBlock, downStreamImports) 123 | } else { 124 | val (template, downStreamImports) = transformCodeTemplate(field) 125 | CodeWithImports.of( 126 | template.safeCall(protoFieldName.escapeIfNecessary()), 127 | downStreamImports 128 | ) 129 | } 130 | 131 | imports.addAll(codeWithImports.imports) 132 | functionBuilder.addStatement("%L", codeWithImports.code) 133 | if (optional) { 134 | functionBuilder.nextControlFlow("else") 135 | functionBuilder.addStatement("null") 136 | functionBuilder.endControlFlowWithComma() 137 | } else { 138 | functionBuilder.addCode(", ") 139 | } 140 | } 141 | functionBuilder.addCode(")") 142 | functionBuilder.addKdoc( 143 | "Converts [%T] to [%T]", 144 | protoType, 145 | generatedType 146 | ) 147 | functionBuilder.addAnnotation( 148 | AnnotationSpec.builder(KrotoDCConverter::class) 149 | .addMember("from = %T::class", protoType) 150 | .addMember("to = %T::class", generatedType) 151 | .build() 152 | ) 153 | 154 | return FunSpecsWithImports( 155 | listOf(functionBuilder.build()), 156 | imports 157 | ) 158 | } 159 | 160 | // code template that could be used like CodeBlock.of(transformCodeTemplate, fieldName) 161 | fun transformCodeTemplate(field: FieldDescriptor): TransformTemplateWithImports { 162 | return when (field.type) { 163 | FieldDescriptor.Type.MESSAGE -> { 164 | messageTypeTransformCodeTemplate(field.messageType) 165 | } 166 | else -> { 167 | TransformTemplateWithImports.of("%L") 168 | } 169 | } 170 | } 171 | 172 | companion object { 173 | fun messageTypeTransformCodeTemplate(descriptor: Descriptor): TransformTemplateWithImports { 174 | return when { 175 | descriptor.isPredefinedType() -> { 176 | preDefinedTypeTransformCodeTemplate(descriptor) 177 | } 178 | else -> TransformTemplateWithImports.of("%L.toDataClass()", setOf(descriptor.toDataClassImport)) 179 | } 180 | } 181 | 182 | private fun preDefinedTypeTransformCodeTemplate( 183 | descriptor: Descriptor 184 | ): TransformTemplateWithImports { 185 | return if (descriptor.isHandledPreDefinedType()) { 186 | HandledPreDefinedType.valueOfByDescriptor(descriptor).toDataClassTemplate 187 | } else { 188 | TransformTemplateWithImports.of("%L") 189 | } 190 | } 191 | } 192 | } 193 | -------------------------------------------------------------------------------- /generator/src/main/kotlin/io/github/mscheong01/krotodc/specgenerators/type/ClientStubGenerator.kt: -------------------------------------------------------------------------------- 1 | // Copyright 2023 Minsoo Cheong 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 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 | package io.github.mscheong01.krotodc.specgenerators.type 15 | 16 | import com.google.protobuf.Descriptors.MethodDescriptor 17 | import com.google.protobuf.Descriptors.ServiceDescriptor 18 | import com.squareup.kotlinpoet.AnnotationSpec 19 | import com.squareup.kotlinpoet.CodeBlock 20 | import com.squareup.kotlinpoet.FunSpec 21 | import com.squareup.kotlinpoet.KModifier 22 | import com.squareup.kotlinpoet.MemberName 23 | import com.squareup.kotlinpoet.MemberName.Companion.member 24 | import com.squareup.kotlinpoet.ParameterSpec 25 | import com.squareup.kotlinpoet.ParameterizedTypeName.Companion.parameterizedBy 26 | import com.squareup.kotlinpoet.TypeSpec 27 | import com.squareup.kotlinpoet.TypeVariableName 28 | import com.squareup.kotlinpoet.asClassName 29 | import io.github.mscheong01.krotodc.import.FunSpecsWithImports 30 | import io.github.mscheong01.krotodc.import.Import 31 | import io.github.mscheong01.krotodc.import.TypeSpecsWithImports 32 | import io.github.mscheong01.krotodc.specgenerators.TypeSpecGenerator 33 | import io.github.mscheong01.krotodc.specgenerators.file.DataClassGenerator 34 | import io.github.mscheong01.krotodc.specgenerators.function.MessageToDataClassFunctionGenerator 35 | import io.github.mscheong01.krotodc.specgenerators.function.MessageToProtoFunctionGenerator 36 | import io.github.mscheong01.krotodc.util.descriptorCode 37 | import io.github.mscheong01.krotodc.util.grpcClass 38 | import io.grpc.kotlin.AbstractCoroutineStub 39 | import io.grpc.kotlin.ClientCalls 40 | import io.grpc.kotlin.StubFor 41 | import kotlinx.coroutines.flow.Flow 42 | 43 | class ClientStubGenerator : TypeSpecGenerator { 44 | override fun generate(descriptor: ServiceDescriptor): TypeSpecsWithImports { 45 | val imports = mutableSetOf() 46 | val ioIdentifier = "Coroutine" 47 | val className = "${descriptor.name}${ioIdentifier}Stub" 48 | val superClassType = 49 | AbstractCoroutineStub::class.asClassName().parameterizedBy(TypeVariableName(className)) 50 | val typeBuilder = TypeSpec.classBuilder(className) 51 | .superclass(superClassType) 52 | .primaryConstructor( 53 | FunSpec.constructorBuilder() 54 | .addParameter("channel", io.grpc.Channel::class) 55 | .addParameter( 56 | ParameterSpec.builder("callOptions", io.grpc.CallOptions::class) 57 | .defaultValue("%M", io.grpc.CallOptions::class.member("DEFAULT")) 58 | .build() 59 | ) 60 | .addAnnotation(JvmOverloads::class) 61 | .build() 62 | ) 63 | .addSuperclassConstructorParameter("channel") 64 | .addSuperclassConstructorParameter("callOptions") 65 | 66 | val clientMethodStubs = descriptor.methods.map { clientMethodStub(it) } 67 | clientMethodStubs.forEach { 68 | it.funSpecs.forEach { function -> typeBuilder.addFunction(function) } 69 | imports.addAll(it.imports) 70 | } 71 | typeBuilder.addFunction( 72 | FunSpec.builder("build") 73 | .addModifiers(KModifier.OVERRIDE) 74 | .addParameter("channel", io.grpc.Channel::class) 75 | .addParameter("callOptions", io.grpc.CallOptions::class) 76 | .returns(TypeVariableName(className)) 77 | .addStatement("return %T(channel, callOptions)", TypeVariableName(className)) 78 | .build() 79 | ) 80 | typeBuilder.addAnnotation( 81 | AnnotationSpec.builder(StubFor::class) 82 | .addMember("%T::class", descriptor.grpcClass) 83 | .build() 84 | ) 85 | 86 | return TypeSpecsWithImports( 87 | listOf(typeBuilder.build()), 88 | imports 89 | ) 90 | } 91 | 92 | fun clientMethodStub(method: MethodDescriptor): FunSpecsWithImports { 93 | val imports = mutableSetOf() 94 | 95 | val inputType = DataClassGenerator 96 | .getTypeNameAndDefaultValue(method.inputType).first.copy(nullable = false) 97 | val outputType = DataClassGenerator 98 | .getTypeNameAndDefaultValue(method.outputType).first.copy(nullable = false) 99 | 100 | val requestParam = if (method.isClientStreaming) { 101 | ParameterSpec.builder("requests", Flow::class.asClassName().parameterizedBy(inputType)).build() 102 | } else { 103 | ParameterSpec.builder("request", inputType).build() 104 | } 105 | val responseType = if (method.isServerStreaming) { 106 | Flow::class.asClassName().parameterizedBy(outputType) 107 | } else { 108 | outputType 109 | } 110 | 111 | val clientFactoryMethod = if (method.isServerStreaming) { 112 | if (method.isClientStreaming) BIDI_STREAMING_CMD else SERVER_STREAMING_CMD 113 | } else { 114 | if (method.isClientStreaming) CLIENT_STREAMING_CMD else UNARY_CMD 115 | } 116 | 117 | val (toDataClassTemplate, toDataClassImports) = MessageToDataClassFunctionGenerator 118 | .messageTypeTransformCodeTemplate(method.outputType) 119 | val (toProtoTemplate, toProtoImports) = MessageToProtoFunctionGenerator 120 | .messageTypeTransformCodeTemplate(method.inputType) 121 | imports.addAll(toDataClassImports) 122 | imports.addAll(toProtoImports) 123 | val requestCode = if (method.isClientStreaming) { 124 | CodeBlock.of( 125 | "requests.map { %L }", 126 | toProtoTemplate.safeCall("it") 127 | ) 128 | } else { 129 | CodeBlock.of( 130 | "%L", 131 | toProtoTemplate.safeCall("request") 132 | ) 133 | } 134 | val implementationCode = if (method.isServerStreaming) { 135 | CodeBlock.of( 136 | """ 137 | return %M( 138 | channel, 139 | %L, 140 | %L, 141 | callOptions, 142 | metadata, 143 | ).map { %L } 144 | """.trimIndent(), 145 | clientFactoryMethod, 146 | method.descriptorCode, 147 | requestCode, 148 | toDataClassTemplate.safeCall("it") 149 | ) 150 | } else { 151 | CodeBlock.of( 152 | """ 153 | return %M( 154 | channel, 155 | %L, 156 | %L, 157 | callOptions, 158 | metadata, 159 | ).let { %L } 160 | """.trimIndent(), 161 | clientFactoryMethod, 162 | method.descriptorCode, 163 | requestCode, 164 | toDataClassTemplate.safeCall("it") 165 | ) 166 | } 167 | val methodBuilder = FunSpec.builder(method.name.decapitalize()) 168 | .addParameter(requestParam) 169 | .returns(responseType) 170 | .addParameter( 171 | ParameterSpec.builder("metadata", io.grpc.Metadata::class) 172 | .defaultValue("%L", "Metadata()") 173 | .build() 174 | ) 175 | .addStatement( 176 | "%L", 177 | implementationCode 178 | ) 179 | .apply { 180 | if (!method.isServerStreaming) { 181 | addModifiers(KModifier.SUSPEND) 182 | } 183 | } 184 | 185 | if (method.options.deprecated) { 186 | methodBuilder.addAnnotation( 187 | AnnotationSpec.builder(Deprecated::class) 188 | .addMember("%S", "The underlying service method is marked deprecated.") 189 | .build() 190 | ) 191 | } 192 | 193 | return FunSpecsWithImports( 194 | listOf(methodBuilder.build()), 195 | imports 196 | ) 197 | } 198 | 199 | companion object { 200 | private val UNARY_CMD: MemberName = ClientCalls::class.member("unaryRpc") 201 | private val CLIENT_STREAMING_CMD: MemberName = 202 | ClientCalls::class.member("clientStreamingRpc") 203 | private val SERVER_STREAMING_CMD: MemberName = 204 | ClientCalls::class.member("serverStreamingRpc") 205 | private val BIDI_STREAMING_CMD: MemberName = 206 | ClientCalls::class.member("bidiStreamingRpc") 207 | } 208 | } 209 | -------------------------------------------------------------------------------- /generator/src/main/kotlin/io/github/mscheong01/krotodc/specgenerators/type/ServiceImplBaseGenerator.kt: -------------------------------------------------------------------------------- 1 | // Copyright 2023 Minsoo Cheong 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 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 | package io.github.mscheong01.krotodc.specgenerators.type 15 | 16 | import com.google.protobuf.Descriptors 17 | import com.google.protobuf.Descriptors.ServiceDescriptor 18 | import com.squareup.kotlinpoet.AnnotationSpec 19 | import com.squareup.kotlinpoet.CodeBlock 20 | import com.squareup.kotlinpoet.FunSpec 21 | import com.squareup.kotlinpoet.KModifier 22 | import com.squareup.kotlinpoet.MemberName 23 | import com.squareup.kotlinpoet.MemberName.Companion.member 24 | import com.squareup.kotlinpoet.ParameterSpec 25 | import com.squareup.kotlinpoet.ParameterizedTypeName.Companion.parameterizedBy 26 | import com.squareup.kotlinpoet.TypeSpec 27 | import com.squareup.kotlinpoet.asClassName 28 | import io.github.mscheong01.krotodc.import.Import 29 | import io.github.mscheong01.krotodc.import.TypeSpecsWithImports 30 | import io.github.mscheong01.krotodc.specgenerators.TypeSpecGenerator 31 | import io.github.mscheong01.krotodc.specgenerators.file.DataClassGenerator 32 | import io.github.mscheong01.krotodc.specgenerators.function.MessageToDataClassFunctionGenerator 33 | import io.github.mscheong01.krotodc.specgenerators.function.MessageToProtoFunctionGenerator 34 | import io.github.mscheong01.krotodc.util.decapitalize 35 | import io.github.mscheong01.krotodc.util.descriptorCode 36 | import io.github.mscheong01.krotodc.util.grpcClass 37 | import io.github.mscheong01.krotodc.util.krotoServiceImplBaseName 38 | import io.grpc.ServerServiceDefinition 39 | import io.grpc.Status 40 | import io.grpc.StatusException 41 | import io.grpc.kotlin.AbstractCoroutineServerImpl 42 | import io.grpc.kotlin.ServerCalls 43 | import kotlinx.coroutines.flow.Flow 44 | import kotlin.coroutines.CoroutineContext 45 | import kotlin.coroutines.EmptyCoroutineContext 46 | 47 | class ServiceImplBaseGenerator : TypeSpecGenerator { 48 | override fun generate(descriptor: ServiceDescriptor): TypeSpecsWithImports { 49 | val serviceImplClassName = descriptor.krotoServiceImplBaseName 50 | 51 | val stubs: List = descriptor.methods.map { serviceMethodStub(it) } 52 | val `super` = AbstractCoroutineServerImpl::class 53 | val coroutineContextParameter = ParameterSpec.builder("coroutineContext", CoroutineContext::class) 54 | .defaultValue("%T", EmptyCoroutineContext::class) 55 | .build() 56 | val implBuilder = TypeSpec 57 | .classBuilder(serviceImplClassName) 58 | .addModifiers(KModifier.ABSTRACT) 59 | .superclass(`super`) 60 | .primaryConstructor( 61 | FunSpec.constructorBuilder() 62 | .addParameter( 63 | coroutineContextParameter 64 | ) 65 | .build() 66 | ) 67 | .addSuperclassConstructorParameter("%N", coroutineContextParameter) 68 | 69 | var serverServiceDefinitionBuilder = 70 | CodeBlock.of( 71 | "%M(%M())", 72 | ServerServiceDefinition::class.member("builder"), 73 | descriptor.grpcClass.member("getServiceDescriptor") 74 | ) 75 | 76 | for (stub in stubs) { 77 | implBuilder.addFunction(stub.methodSpec) 78 | serverServiceDefinitionBuilder = CodeBlock.of( 79 | """ 80 | %L 81 | .addMethod(%L) 82 | """.trimIndent(), 83 | serverServiceDefinitionBuilder, 84 | stub.serverMethodDef 85 | ) 86 | } 87 | 88 | implBuilder.addFunction( 89 | FunSpec.builder("bindService") 90 | .addModifiers(KModifier.OVERRIDE, KModifier.FINAL) 91 | .returns(ServerServiceDefinition::class) 92 | .addStatement("return %L.build()", serverServiceDefinitionBuilder) 93 | .build() 94 | ) 95 | 96 | return TypeSpecsWithImports( 97 | typeSpecs = listOf(implBuilder.build()), 98 | imports = stubs.map { it.imports }.flatten().toSet() 99 | ) 100 | } 101 | 102 | fun serviceMethodStub(method: Descriptors.MethodDescriptor): MethodStub { 103 | val imports = mutableSetOf() 104 | val inputType = DataClassGenerator 105 | .getTypeNameAndDefaultValue(method.inputType).first.copy(nullable = false) 106 | val outputType = DataClassGenerator 107 | .getTypeNameAndDefaultValue(method.outputType).first.copy(nullable = false) 108 | 109 | val requestParam = if (method.isClientStreaming) { 110 | ParameterSpec.builder("requests", Flow::class.asClassName().parameterizedBy(inputType)).build() 111 | } else { 112 | ParameterSpec.builder("request", inputType).build() 113 | } 114 | 115 | val methodBuilder = FunSpec.builder(method.name.decapitalize()) 116 | .addModifiers(KModifier.OPEN) 117 | .addParameter(requestParam) 118 | .addStatement( 119 | "throw %T(%M.withDescription(%S))", 120 | StatusException::class, 121 | Status::class.member("UNIMPLEMENTED"), 122 | "Method ${method.fullName} is unimplemented" 123 | ) 124 | 125 | if (method.options.deprecated) { 126 | methodBuilder.addAnnotation( 127 | AnnotationSpec.builder(Deprecated::class) 128 | .addMember("%S", "The underlying service method is marked deprecated.") 129 | .build() 130 | ) 131 | } 132 | 133 | if (method.isServerStreaming) { 134 | methodBuilder.returns(Flow::class.asClassName().parameterizedBy(outputType)) 135 | } else { 136 | methodBuilder.returns(outputType) 137 | methodBuilder.addModifiers(KModifier.SUSPEND) 138 | } 139 | 140 | val methodSpec = methodBuilder.build() 141 | 142 | val smdFactory = if (method.isServerStreaming) { 143 | if (method.isClientStreaming) BIDI_STREAMING_SMD else SERVER_STREAMING_SMD 144 | } else { 145 | if (method.isClientStreaming) CLIENT_STREAMING_SMD else UNARY_SMD 146 | } 147 | 148 | val (requestTransformTemplate, reqImports) = MessageToDataClassFunctionGenerator 149 | .messageTypeTransformCodeTemplate(method.inputType) 150 | val (responseTransformTemplate, resImports) = MessageToProtoFunctionGenerator 151 | .messageTypeTransformCodeTemplate(method.outputType) 152 | imports.addAll(reqImports) 153 | imports.addAll(resImports) 154 | 155 | val implementationCode = if (method.isServerStreaming) { 156 | if (method.isClientStreaming) { 157 | CodeBlock.of( 158 | """ 159 | { requests -> 160 | %N(requests.map { %L }).map { %L } 161 | } 162 | """.trimIndent(), 163 | methodSpec, 164 | requestTransformTemplate.safeCall("it"), 165 | responseTransformTemplate.safeCall("it") 166 | ) 167 | } else { 168 | CodeBlock.of( 169 | """ 170 | { request -> 171 | %N(%L).map { %L } 172 | } 173 | """.trimIndent(), 174 | methodSpec, 175 | requestTransformTemplate.safeCall("request"), 176 | responseTransformTemplate.safeCall("it") 177 | ) 178 | } 179 | } else { 180 | if (method.isClientStreaming) { 181 | CodeBlock.of( 182 | """ 183 | { requests -> 184 | %N(requests.map { %L }).let { %L } 185 | } 186 | """.trimIndent(), 187 | methodSpec, 188 | requestTransformTemplate.safeCall("it"), 189 | responseTransformTemplate.safeCall("it") 190 | ) 191 | } else { 192 | CodeBlock.of( 193 | """ 194 | { request -> 195 | %N(%L).let { %L } 196 | } 197 | """.trimIndent(), 198 | methodSpec, 199 | requestTransformTemplate.safeCall("request"), 200 | responseTransformTemplate.safeCall("it") 201 | ) 202 | } 203 | } 204 | 205 | val serverMethodDef = CodeBlock.of( 206 | """ 207 | %M( 208 | context = this.context, 209 | descriptor = %L, 210 | implementation = %L, 211 | ) 212 | """.trimIndent(), 213 | smdFactory, 214 | method.descriptorCode, 215 | implementationCode 216 | ) 217 | 218 | return MethodStub(methodSpec, serverMethodDef, imports) 219 | } 220 | 221 | data class MethodStub( 222 | val methodSpec: FunSpec, 223 | /** 224 | * A [CodeBlock] that computes a [ServerMethodDefinition] based on an implementation of 225 | * the function described in [methodSpec]. 226 | */ 227 | val serverMethodDef: CodeBlock, 228 | val imports: Set 229 | ) 230 | 231 | companion object { 232 | private val UNARY_SMD: MemberName = ServerCalls::class.member("unaryServerMethodDefinition") 233 | private val CLIENT_STREAMING_SMD: MemberName = 234 | ServerCalls::class.member("clientStreamingServerMethodDefinition") 235 | private val SERVER_STREAMING_SMD: MemberName = 236 | ServerCalls::class.member("serverStreamingServerMethodDefinition") 237 | private val BIDI_STREAMING_SMD: MemberName = 238 | ServerCalls::class.member("bidiStreamingServerMethodDefinition") 239 | } 240 | } 241 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | 2 | Apache License 3 | Version 2.0, January 2004 4 | http://www.apache.org/licenses/ 5 | 6 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 7 | 8 | 1. Definitions. 9 | 10 | "License" shall mean the terms and conditions for use, reproduction, 11 | and distribution as defined by Sections 1 through 9 of this document. 12 | 13 | "Licensor" shall mean the copyright owner or entity authorized by 14 | the copyright owner that is granting the License. 15 | 16 | "Legal Entity" shall mean the union of the acting entity and all 17 | other entities that control, are controlled by, or are under common 18 | control with that entity. For the purposes of this definition, 19 | "control" means (i) the power, direct or indirect, to cause the 20 | direction or management of such entity, whether by contract or 21 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 22 | outstanding shares, or (iii) beneficial ownership of such entity. 23 | 24 | "You" (or "Your") shall mean an individual or Legal Entity 25 | exercising permissions granted by this License. 26 | 27 | "Source" form shall mean the preferred form for making modifications, 28 | including but not limited to software source code, documentation 29 | source, and configuration files. 30 | 31 | "Object" form shall mean any form resulting from mechanical 32 | transformation or translation of a Source form, including but 33 | not limited to compiled object code, generated documentation, 34 | and conversions to other media types. 35 | 36 | "Work" shall mean the work of authorship, whether in Source or 37 | Object form, made available under the License, as indicated by a 38 | copyright notice that is included in or attached to the work 39 | (an example is provided in the Appendix below). 40 | 41 | "Derivative Works" shall mean any work, whether in Source or Object 42 | form, that is based on (or derived from) the Work and for which the 43 | editorial revisions, annotations, elaborations, or other modifications 44 | represent, as a whole, an original work of authorship. For the purposes 45 | of this License, Derivative Works shall not include works that remain 46 | separable from, or merely link (or bind by name) to the interfaces of, 47 | the Work and Derivative Works thereof. 48 | 49 | "Contribution" shall mean any work of authorship, including 50 | the original version of the Work and any modifications or additions 51 | to that Work or Derivative Works thereof, that is intentionally 52 | submitted to Licensor for inclusion in the Work by the copyright owner 53 | or by an individual or Legal Entity authorized to submit on behalf of 54 | the copyright owner. For the purposes of this definition, "submitted" 55 | means any form of electronic, verbal, or written communication sent 56 | to the Licensor or its representatives, including but not limited to 57 | communication on electronic mailing lists, source code control systems, 58 | and issue tracking systems that are managed by, or on behalf of, the 59 | Licensor for the purpose of discussing and improving the Work, but 60 | excluding communication that is conspicuously marked or otherwise 61 | designated in writing by the copyright owner as "Not a Contribution." 62 | 63 | "Contributor" shall mean Licensor and any individual or Legal Entity 64 | on behalf of whom a Contribution has been received by Licensor and 65 | subsequently incorporated within the Work. 66 | 67 | 2. Grant of Copyright License. Subject to the terms and conditions of 68 | this License, each Contributor hereby grants to You a perpetual, 69 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 70 | copyright license to reproduce, prepare Derivative Works of, 71 | publicly display, publicly perform, sublicense, and distribute the 72 | Work and such Derivative Works in Source or Object form. 73 | 74 | 3. Grant of Patent License. Subject to the terms and conditions of 75 | this License, each Contributor hereby grants to You a perpetual, 76 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 77 | (except as stated in this section) patent license to make, have made, 78 | use, offer to sell, sell, import, and otherwise transfer the Work, 79 | where such license applies only to those patent claims licensable 80 | by such Contributor that are necessarily infringed by their 81 | Contribution(s) alone or by combination of their Contribution(s) 82 | with the Work to which such Contribution(s) was submitted. If You 83 | institute patent litigation against any entity (including a 84 | cross-claim or counterclaim in a lawsuit) alleging that the Work 85 | or a Contribution incorporated within the Work constitutes direct 86 | or contributory patent infringement, then any patent licenses 87 | granted to You under this License for that Work shall terminate 88 | as of the date such litigation is filed. 89 | 90 | 4. Redistribution. You may reproduce and distribute copies of the 91 | Work or Derivative Works thereof in any medium, with or without 92 | modifications, and in Source or Object form, provided that You 93 | meet the following conditions: 94 | 95 | (a) You must give any other recipients of the Work or 96 | Derivative Works a copy of this License; and 97 | 98 | (b) You must cause any modified files to carry prominent notices 99 | stating that You changed the files; and 100 | 101 | (c) You must retain, in the Source form of any Derivative Works 102 | that You distribute, all copyright, patent, trademark, and 103 | attribution notices from the Source form of the Work, 104 | excluding those notices that do not pertain to any part of 105 | the Derivative Works; and 106 | 107 | (d) If the Work includes a "NOTICE" text file as part of its 108 | distribution, then any Derivative Works that You distribute must 109 | include a readable copy of the attribution notices contained 110 | within such NOTICE file, excluding those notices that do not 111 | pertain to any part of the Derivative Works, in at least one 112 | of the following places: within a NOTICE text file distributed 113 | as part of the Derivative Works; within the Source form or 114 | documentation, if provided along with the Derivative Works; or, 115 | within a display generated by the Derivative Works, if and 116 | wherever such third-party notices normally appear. The contents 117 | of the NOTICE file are for informational purposes only and 118 | do not modify the License. You may add Your own attribution 119 | notices within Derivative Works that You distribute, alongside 120 | or as an addendum to the NOTICE text from the Work, provided 121 | that such additional attribution notices cannot be construed 122 | as modifying the License. 123 | 124 | You may add Your own copyright statement to Your modifications and 125 | may provide additional or different license terms and conditions 126 | for use, reproduction, or distribution of Your modifications, or 127 | for any such Derivative Works as a whole, provided Your use, 128 | reproduction, and distribution of the Work otherwise complies with 129 | the conditions stated in this License. 130 | 131 | 5. Submission of Contributions. Unless You explicitly state otherwise, 132 | any Contribution intentionally submitted for inclusion in the Work 133 | by You to the Licensor shall be under the terms and conditions of 134 | this License, without any additional terms or conditions. 135 | Notwithstanding the above, nothing herein shall supersede or modify 136 | the terms of any separate license agreement you may have executed 137 | with Licensor regarding such Contributions. 138 | 139 | 6. Trademarks. This License does not grant permission to use the trade 140 | names, trademarks, service marks, or product names of the Licensor, 141 | except as required for reasonable and customary use in describing the 142 | origin of the Work and reproducing the content of the NOTICE file. 143 | 144 | 7. Disclaimer of Warranty. Unless required by applicable law or 145 | agreed to in writing, Licensor provides the Work (and each 146 | Contributor provides its Contributions) on an "AS IS" BASIS, 147 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 148 | implied, including, without limitation, any warranties or conditions 149 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 150 | PARTICULAR PURPOSE. You are solely responsible for determining the 151 | appropriateness of using or redistributing the Work and assume any 152 | risks associated with Your exercise of permissions under this License. 153 | 154 | 8. Limitation of Liability. In no event and under no legal theory, 155 | whether in tort (including negligence), contract, or otherwise, 156 | unless required by applicable law (such as deliberate and grossly 157 | negligent acts) or agreed to in writing, shall any Contributor be 158 | liable to You for damages, including any direct, indirect, special, 159 | incidental, or consequential damages of any character arising as a 160 | result of this License or out of the use or inability to use the 161 | Work (including but not limited to damages for loss of goodwill, 162 | work stoppage, computer failure or malfunction, or any and all 163 | other commercial damages or losses), even if such Contributor 164 | has been advised of the possibility of such damages. 165 | 166 | 9. Accepting Warranty or Additional Liability. While redistributing 167 | the Work or Derivative Works thereof, You may choose to offer, 168 | and charge a fee for, acceptance of support, warranty, indemnity, 169 | or other liability obligations and/or rights consistent with this 170 | License. However, in accepting such obligations, You may act only 171 | on Your own behalf and on Your sole responsibility, not on behalf 172 | of any other Contributor, and only if You agree to indemnify, 173 | defend, and hold each Contributor harmless for any liability 174 | incurred by, or claims asserted against, such Contributor by reason 175 | of your accepting any such warranty or additional liability. 176 | 177 | END OF TERMS AND CONDITIONS 178 | 179 | APPENDIX: How to apply the Apache License to your work. 180 | 181 | To apply the Apache License to your work, attach the following 182 | boilerplate notice, with the fields enclosed by brackets "[]" 183 | replaced with your own identifying information. (Don't include 184 | the brackets!) The text should be enclosed in the appropriate 185 | comment syntax for the file format. We also recommend that a 186 | file or class name and description of purpose be included on the 187 | same "printed page" as the copyright notice for easier 188 | identification within third-party archives. 189 | 190 | Copyright [yyyy] [name of copyright owner] 191 | 192 | Licensed under the Apache License, Version 2.0 (the "License"); 193 | you may not use this file except in compliance with the License. 194 | You may obtain a copy of the License at 195 | 196 | http://www.apache.org/licenses/LICENSE-2.0 197 | 198 | Unless required by applicable law or agreed to in writing, software 199 | distributed under the License is distributed on an "AS IS" BASIS, 200 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 201 | See the License for the specific language governing permissions and 202 | limitations under the License. 203 | -------------------------------------------------------------------------------- /generator/src/main/kotlin/io/github/mscheong01/krotodc/specgenerators/file/DataClassGenerator.kt: -------------------------------------------------------------------------------- 1 | // Copyright 2023 Minsoo Cheong 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 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 | package io.github.mscheong01.krotodc.specgenerators.file 15 | 16 | import com.google.protobuf.Descriptors 17 | import com.google.protobuf.Descriptors.Descriptor 18 | import com.google.protobuf.Descriptors.FileDescriptor 19 | import com.squareup.kotlinpoet.AnnotationSpec 20 | import com.squareup.kotlinpoet.BOOLEAN 21 | import com.squareup.kotlinpoet.ClassName 22 | import com.squareup.kotlinpoet.DOUBLE 23 | import com.squareup.kotlinpoet.FLOAT 24 | import com.squareup.kotlinpoet.FileSpec 25 | import com.squareup.kotlinpoet.FunSpec 26 | import com.squareup.kotlinpoet.INT 27 | import com.squareup.kotlinpoet.KModifier 28 | import com.squareup.kotlinpoet.LIST 29 | import com.squareup.kotlinpoet.LONG 30 | import com.squareup.kotlinpoet.MAP 31 | import com.squareup.kotlinpoet.ParameterSpec 32 | import com.squareup.kotlinpoet.ParameterizedTypeName.Companion.parameterizedBy 33 | import com.squareup.kotlinpoet.PropertySpec 34 | import com.squareup.kotlinpoet.STRING 35 | import com.squareup.kotlinpoet.TypeName 36 | import com.squareup.kotlinpoet.TypeSpec 37 | import io.github.mscheong01.krotodc.KrotoDC 38 | import io.github.mscheong01.krotodc.import.CodeWithImports 39 | import io.github.mscheong01.krotodc.import.Import 40 | import io.github.mscheong01.krotodc.import.TypeSpecsWithImports 41 | import io.github.mscheong01.krotodc.predefinedtypes.HandledPreDefinedType 42 | import io.github.mscheong01.krotodc.specgenerators.FileSpecGenerator 43 | import io.github.mscheong01.krotodc.util.MAP_ENTRY_KEY_FIELD_NUMBER 44 | import io.github.mscheong01.krotodc.util.MAP_ENTRY_VALUE_FIELD_NUMBER 45 | import io.github.mscheong01.krotodc.util.addAllImports 46 | import io.github.mscheong01.krotodc.util.capitalize 47 | import io.github.mscheong01.krotodc.util.fieldNameToJsonName 48 | import io.github.mscheong01.krotodc.util.isHandledPreDefinedType 49 | import io.github.mscheong01.krotodc.util.isKrotoDCOptional 50 | import io.github.mscheong01.krotodc.util.isPredefinedType 51 | import io.github.mscheong01.krotodc.util.krotoDCPackage 52 | import io.github.mscheong01.krotodc.util.krotoDCTypeName 53 | import io.github.mscheong01.krotodc.util.protobufJavaTypeName 54 | import io.github.mscheong01.krotodc.util.simpleNames 55 | 56 | class DataClassGenerator : FileSpecGenerator { 57 | 58 | override fun generate(fileDescriptor: FileDescriptor): List { 59 | val fileSpecs = mutableMapOf() 60 | for (messageType in fileDescriptor.messageTypes) { 61 | val fileSpecBuilder = FileSpec.builder(fileDescriptor.krotoDCPackage, messageType.name + ".kt") 62 | val specsWithImports = generateTypeSpecForMessageDescriptor(messageType) 63 | specsWithImports.typeSpecs.forEach { fileSpecBuilder.addType(it) } 64 | fileSpecBuilder.addAllImports(specsWithImports.imports) 65 | if (fileSpecBuilder.members.isNotEmpty()) { 66 | fileSpecs[messageType.fullName] = fileSpecBuilder.build() 67 | } 68 | } 69 | return fileSpecs.values.toList() 70 | } 71 | 72 | fun generateTypeSpecForMessageDescriptor( 73 | messageDescriptor: Descriptor 74 | ): TypeSpecsWithImports { 75 | if ( 76 | messageDescriptor.isPredefinedType() || 77 | messageDescriptor.options.mapEntry 78 | ) { 79 | return TypeSpecsWithImports.EMPTY 80 | } 81 | val imports = mutableSetOf() 82 | val className = messageDescriptor.krotoDCTypeName 83 | val dataClassBuilder = TypeSpec.classBuilder(className) 84 | .apply { 85 | if (messageDescriptor.fields.isNotEmpty()) { 86 | addModifiers(KModifier.DATA) 87 | } 88 | } 89 | val constructorBuilder = FunSpec.constructorBuilder() 90 | 91 | for (nestedType in messageDescriptor.nestedTypes) { 92 | generateTypeSpecForMessageDescriptor(nestedType).let { 93 | imports.addAll(it.imports) 94 | it.typeSpecs.forEach { dataClassBuilder.addType(it) } 95 | } 96 | } 97 | for (nestedEnum in messageDescriptor.enumTypes) { 98 | val enumJavaTypeName = nestedEnum.protobufJavaTypeName 99 | imports.add(Import(enumJavaTypeName.packageName, enumJavaTypeName.simpleNames)) 100 | } 101 | 102 | messageDescriptor.realOneofs.forEach { oneOf -> 103 | val oneOfJsonName = fieldNameToJsonName(oneOf.name) 104 | val interfaceClassName = ClassName( 105 | oneOf.file.krotoDCPackage, 106 | *messageDescriptor.simpleNames.toMutableList().apply { 107 | add(oneOfJsonName.capitalize()) 108 | }.toTypedArray() 109 | ) 110 | val builder = TypeSpec.interfaceBuilder(interfaceClassName) 111 | .addModifiers(KModifier.SEALED) 112 | 113 | oneOf.fields.forEach { 114 | val (type, default) = mapProtoTypeToKotlinTypeAndDefaultValue(it) 115 | imports.addAll(default.imports) 116 | builder.addType( 117 | TypeSpec.classBuilder(it.jsonName.capitalize()) 118 | .addModifiers(KModifier.DATA) 119 | .primaryConstructor( 120 | FunSpec.constructorBuilder() 121 | .addParameter( 122 | ParameterSpec.builder( 123 | it.jsonName, 124 | type 125 | ).apply { 126 | if (it.isKrotoDCOptional) { 127 | defaultValue(default.code) 128 | } 129 | }.build() 130 | ).build() 131 | ) 132 | .addProperty( 133 | PropertySpec.builder( 134 | it.jsonName, 135 | type 136 | ).initializer(it.jsonName).build() 137 | ) 138 | .apply { 139 | if (it.options.deprecated) { 140 | addAnnotation( 141 | AnnotationSpec.builder(Deprecated::class) 142 | .addMember("%S", "The underlying field is marked deprecated.") 143 | .build() 144 | ) 145 | } 146 | } 147 | .addSuperinterface(interfaceClassName) 148 | .build() 149 | ) 150 | } 151 | 152 | constructorBuilder.addParameter( 153 | ParameterSpec.builder( 154 | oneOfJsonName, 155 | interfaceClassName.copy(nullable = true) 156 | ).defaultValue("null").build() 157 | ) 158 | dataClassBuilder.addProperty( 159 | PropertySpec.builder( 160 | oneOfJsonName, 161 | interfaceClassName.copy(nullable = true) 162 | ).initializer(oneOfJsonName).build() 163 | ) 164 | dataClassBuilder.addType(builder.build()) 165 | } 166 | 167 | for (field in messageDescriptor.fields) { 168 | if ( 169 | field.name in messageDescriptor.realOneofs.map { it.fields }.flatten().map { it.name }.toSet() || 170 | field.isExtension 171 | ) { 172 | continue 173 | } 174 | val fieldName = field.jsonName 175 | val (fieldType, default) = mapProtoTypeToKotlinTypeAndDefaultValue(field) 176 | imports.addAll(default.imports) 177 | constructorBuilder 178 | .addParameter( 179 | ParameterSpec 180 | .builder( 181 | fieldName, 182 | fieldType 183 | ) 184 | .apply { 185 | if (field.options.deprecated) { 186 | addAnnotation( 187 | AnnotationSpec.builder(Deprecated::class) 188 | .addMember("%S", "The underlying message field is marked deprecated.") 189 | .build() 190 | ) 191 | } 192 | } 193 | .apply { 194 | if (field.isKrotoDCOptional) { 195 | defaultValue(default.code) 196 | } 197 | }.build() 198 | ) 199 | dataClassBuilder.addProperty( 200 | PropertySpec.builder( 201 | fieldName, 202 | fieldType 203 | ).initializer(fieldName).build() 204 | ) 205 | } 206 | 207 | dataClassBuilder.primaryConstructor(constructorBuilder.build()) 208 | dataClassBuilder.addAnnotation( 209 | AnnotationSpec.builder(KrotoDC::class) 210 | .addMember("forProto = %L::class", messageDescriptor.protobufJavaTypeName) 211 | .build() 212 | ) 213 | 214 | dataClassBuilder.addType(TypeSpec.companionObjectBuilder().build()) 215 | return TypeSpecsWithImports( 216 | listOf(dataClassBuilder.build()), 217 | imports 218 | ) 219 | } 220 | 221 | private fun mapProtoTypeToKotlinTypeAndDefaultValue(field: Descriptors.FieldDescriptor): Pair { 222 | var (poetType, defaultValue) = when ( 223 | field.javaType ?: throw IllegalStateException("Field $field does not have a java type") 224 | ) { 225 | Descriptors.FieldDescriptor.JavaType.INT -> Pair(INT, CodeWithImports.of("0")) 226 | Descriptors.FieldDescriptor.JavaType.LONG -> Pair(LONG, CodeWithImports.of("0L")) 227 | Descriptors.FieldDescriptor.JavaType.FLOAT -> Pair(FLOAT, CodeWithImports.of("0.0f")) 228 | Descriptors.FieldDescriptor.JavaType.DOUBLE -> Pair(DOUBLE, CodeWithImports.of("0.0")) 229 | Descriptors.FieldDescriptor.JavaType.BOOLEAN -> Pair(BOOLEAN, CodeWithImports.of("false")) 230 | Descriptors.FieldDescriptor.JavaType.STRING -> Pair(STRING, CodeWithImports.of("\"\"")) 231 | Descriptors.FieldDescriptor.JavaType.BYTE_STRING -> Pair( 232 | ClassName("com.google.protobuf", "ByteString"), 233 | CodeWithImports.of("com.google.protobuf.ByteString.EMPTY") 234 | ) 235 | 236 | Descriptors.FieldDescriptor.JavaType.ENUM -> { 237 | val enumType = field.enumType 238 | ?: throw IllegalStateException("Enum field $field does not have an enum type") 239 | Pair( 240 | enumType.protobufJavaTypeName, 241 | CodeWithImports.of("${enumType.protobufJavaTypeName.canonicalName}.values()[0]") 242 | ) 243 | } 244 | 245 | Descriptors.FieldDescriptor.JavaType.MESSAGE -> { 246 | if (field.isMapField) { 247 | val keyType = field.messageType.findFieldByNumber(MAP_ENTRY_KEY_FIELD_NUMBER) 248 | ?.let { this.mapProtoTypeToKotlinTypeAndDefaultValue(it).first } 249 | ?: throw IllegalStateException("Map field $field does not have a key field") 250 | val valueType = field.messageType.findFieldByNumber(MAP_ENTRY_VALUE_FIELD_NUMBER) 251 | ?.let { this.mapProtoTypeToKotlinTypeAndDefaultValue(it).first } 252 | ?: throw IllegalStateException("Map field $field does not have a value field") 253 | val type = MAP.parameterizedBy( 254 | keyType.copy(nullable = false), 255 | valueType.copy(nullable = false) 256 | ) 257 | Pair(type, CodeWithImports.of("mapOf()")) 258 | } else { 259 | getTypeNameAndDefaultValue(field.messageType) 260 | } 261 | } 262 | } 263 | if (field.isRepeated && !field.isMapField) { 264 | poetType = LIST.parameterizedBy(poetType.copy(nullable = false)) 265 | defaultValue = CodeWithImports.of("listOf()") 266 | } 267 | return if (field.isKrotoDCOptional) { 268 | Pair(poetType.copy(nullable = true), CodeWithImports.of("null")) 269 | } else { 270 | Pair(poetType.copy(nullable = false), defaultValue) 271 | } 272 | } 273 | 274 | companion object { 275 | /** 276 | * Returns the Kotlin type name and default value for the given descriptor. 277 | * This method should not be called on Map field messageTypes 278 | */ 279 | fun getTypeNameAndDefaultValue( 280 | descriptor: Descriptor 281 | ): Pair { 282 | val generatedTypeName = descriptor.krotoDCTypeName 283 | if (descriptor.isPredefinedType()) { 284 | return getTypeNameAndDefaultValueForPreDefinedTypes(descriptor) 285 | } 286 | return Pair(generatedTypeName, CodeWithImports.of("${generatedTypeName.canonicalName}()")) 287 | } 288 | 289 | private fun getTypeNameAndDefaultValueForPreDefinedTypes( 290 | descriptor: Descriptor 291 | ): Pair { 292 | return if (descriptor.isHandledPreDefinedType()) { 293 | HandledPreDefinedType.valueOfByDescriptor(descriptor).let { 294 | it.kotlinType to it.defaultValue 295 | } 296 | } else { 297 | Pair( 298 | descriptor.protobufJavaTypeName, 299 | CodeWithImports.of("${descriptor.protobufJavaTypeName.canonicalName}.getDefaultInstance()") 300 | ) 301 | } 302 | } 303 | } 304 | } 305 | -------------------------------------------------------------------------------- /generator/src/test/kotlin/io/github/mscheong01/krotodc/ConversionTest.kt: -------------------------------------------------------------------------------- 1 | // Copyright 2023 Minsoo Cheong 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 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 | package io.github.mscheong01.krotodc 15 | 16 | import com.example.importtest.OuterClassNameTestProto 17 | import com.google.protobuf.ByteString 18 | import io.github.mscheong01.importtest.ImportFromOtherFileTest.ImportTestMessage 19 | import io.github.mscheong01.importtest.krotodc.importtestmessage.toDataClass 20 | import io.github.mscheong01.importtest.krotodc.importtestmessage.toProto 21 | import io.github.mscheong01.test.DeprecatedMessage 22 | import io.github.mscheong01.test.Employee 23 | import io.github.mscheong01.test.EmptyMessage 24 | import io.github.mscheong01.test.Job 25 | import io.github.mscheong01.test.MapMessage 26 | import io.github.mscheong01.test.OneOfMessage 27 | import io.github.mscheong01.test.OptionalMessage 28 | import io.github.mscheong01.test.Person 29 | import io.github.mscheong01.test.PrimitiveMessage 30 | import io.github.mscheong01.test.RepeatedMessage 31 | import io.github.mscheong01.test.TopLevelMessage 32 | import io.github.mscheong01.test.krotodc.deprecatedmessage.toDataClass 33 | import io.github.mscheong01.test.krotodc.employee.toDataClass 34 | import io.github.mscheong01.test.krotodc.employee.toProto 35 | import io.github.mscheong01.test.krotodc.emptymessage.toDataClass 36 | import io.github.mscheong01.test.krotodc.emptymessage.toProto 37 | import io.github.mscheong01.test.krotodc.mapmessage.toDataClass 38 | import io.github.mscheong01.test.krotodc.mapmessage.toProto 39 | import io.github.mscheong01.test.krotodc.oneofmessage.toDataClass 40 | import io.github.mscheong01.test.krotodc.oneofmessage.toProto 41 | import io.github.mscheong01.test.krotodc.optionalmessage.toDataClass 42 | import io.github.mscheong01.test.krotodc.optionalmessage.toProto 43 | import io.github.mscheong01.test.krotodc.person.toDataClass 44 | import io.github.mscheong01.test.krotodc.person.toProto 45 | import io.github.mscheong01.test.krotodc.primitivemessage.toDataClass 46 | import io.github.mscheong01.test.krotodc.primitivemessage.toProto 47 | import io.github.mscheong01.test.krotodc.repeatedmessage.toDataClass 48 | import io.github.mscheong01.test.krotodc.repeatedmessage.toProto 49 | import io.github.mscheong01.test.krotodc.toplevelmessage.toDataClass 50 | import io.github.mscheong01.test.krotodc.toplevelmessage.toProto 51 | import org.assertj.core.api.Assertions 52 | import org.junit.jupiter.api.Test 53 | import java.util.Map.entry 54 | 55 | class ConversionTest { 56 | 57 | @Test 58 | fun `test simple message`() { 59 | val proto = Person.newBuilder() 60 | .setName("John") 61 | .setAge(30) 62 | .build() 63 | val kroto = proto.toDataClass() 64 | Assertions.assertThat(kroto.name).isEqualTo("John") 65 | Assertions.assertThat(kroto.age).isEqualTo(30) 66 | val proto2 = kroto.toProto() 67 | Assertions.assertThat(proto2.name).isEqualTo("John") 68 | Assertions.assertThat(proto2.age).isEqualTo(30) 69 | Assertions.assertThat(proto2).isEqualTo(proto) 70 | } 71 | 72 | @Test 73 | fun `test message with enum`() { 74 | val proto = Employee.newBuilder() 75 | .setName("John") 76 | .setAge(30) 77 | .setJob(Job.ENGINEER) 78 | .build() 79 | val kroto = proto.toDataClass() 80 | Assertions.assertThat(kroto.name).isEqualTo("John") 81 | Assertions.assertThat(kroto.age).isEqualTo(30) 82 | Assertions.assertThat(kroto.job).isEqualTo(Job.ENGINEER) 83 | val proto2 = kroto.toProto() 84 | Assertions.assertThat(proto2.name).isEqualTo("John") 85 | Assertions.assertThat(proto2.age).isEqualTo(30) 86 | Assertions.assertThat(proto2.job).isEqualTo(Job.ENGINEER) 87 | Assertions.assertThat(proto2).isEqualTo(proto) 88 | } 89 | 90 | @Test 91 | fun `test message with nested types`() { 92 | val proto = TopLevelMessage.newBuilder() 93 | .setNestedMessage(TopLevelMessage.NestedMessage.newBuilder().setName("John").build()) 94 | .setNestedEnum(TopLevelMessage.NestedEnum.A) 95 | .build() 96 | val kroto = proto.toDataClass() 97 | Assertions.assertThat(kroto.nestedMessage.name).isEqualTo("John") 98 | Assertions.assertThat(kroto.nestedEnum).isEqualTo(TopLevelMessage.NestedEnum.A) 99 | val proto2 = kroto.toProto() 100 | Assertions.assertThat(proto2.nestedMessage.name).isEqualTo("John") 101 | Assertions.assertThat(proto2.nestedEnum).isEqualTo(TopLevelMessage.NestedEnum.A) 102 | Assertions.assertThat(proto2).isEqualTo(proto) 103 | } 104 | 105 | @Test 106 | fun `test message with all primitive types`() { 107 | val proto = PrimitiveMessage.newBuilder() 108 | .setInt32Field(1).setInt64Field(2).setUint32Field(3).setUint64Field(4).setSint32Field(5) 109 | .setSint64Field(6).setFixed32Field(7).setFixed64Field(8).setSfixed32Field(9) 110 | .setSfixed64Field(10).setFloatField(11.0f).setDoubleField(12.0).setBoolField(true) 111 | .setStringField("13").setBytesField(ByteString.copyFromUtf8("14")).build() 112 | val kroto = proto.toDataClass() 113 | Assertions.assertThat(kroto.int32Field).isEqualTo(1) 114 | Assertions.assertThat(kroto.int64Field).isEqualTo(2) 115 | Assertions.assertThat(kroto.uint32Field).isEqualTo(3) 116 | Assertions.assertThat(kroto.uint64Field).isEqualTo(4) 117 | Assertions.assertThat(kroto.sint32Field).isEqualTo(5) 118 | Assertions.assertThat(kroto.sint64Field).isEqualTo(6) 119 | Assertions.assertThat(kroto.fixed32Field).isEqualTo(7) 120 | Assertions.assertThat(kroto.fixed64Field).isEqualTo(8) 121 | Assertions.assertThat(kroto.sfixed32Field).isEqualTo(9) 122 | Assertions.assertThat(kroto.sfixed64Field).isEqualTo(10) 123 | Assertions.assertThat(kroto.floatField).isEqualTo(11.0f) 124 | Assertions.assertThat(kroto.doubleField).isEqualTo(12.0) 125 | Assertions.assertThat(kroto.boolField).isEqualTo(true) 126 | Assertions.assertThat(kroto.stringField).isEqualTo("13") 127 | Assertions.assertThat(kroto.bytesField).isEqualTo(ByteString.copyFromUtf8("14")) 128 | val proto2 = kroto.toProto() 129 | Assertions.assertThat(proto2.int32Field).isEqualTo(1) 130 | Assertions.assertThat(proto2.int64Field).isEqualTo(2) 131 | Assertions.assertThat(proto2.uint32Field).isEqualTo(3) 132 | Assertions.assertThat(proto2.uint64Field).isEqualTo(4) 133 | Assertions.assertThat(proto2.sint32Field).isEqualTo(5) 134 | Assertions.assertThat(proto2.sint64Field).isEqualTo(6) 135 | Assertions.assertThat(proto2.fixed32Field).isEqualTo(7) 136 | Assertions.assertThat(proto2.fixed64Field).isEqualTo(8) 137 | Assertions.assertThat(proto2.sfixed32Field).isEqualTo(9) 138 | Assertions.assertThat(proto2.sfixed64Field).isEqualTo(10) 139 | Assertions.assertThat(proto2.floatField).isEqualTo(11.0f) 140 | Assertions.assertThat(proto2.doubleField).isEqualTo(12.0) 141 | Assertions.assertThat(proto2.boolField).isEqualTo(true) 142 | Assertions.assertThat(proto2.stringField).isEqualTo("13") 143 | Assertions.assertThat(proto2.bytesField).isEqualTo(ByteString.copyFromUtf8("14")) 144 | Assertions.assertThat(proto2).isEqualTo(proto) 145 | } 146 | 147 | @Test 148 | fun `test message with repeated fields`() { 149 | val proto = RepeatedMessage.newBuilder() 150 | .addAllRepeatedString(listOf("1", "2", "3")) 151 | .addAllRepeatedJob(listOf(Job.ENGINEER, Job.PRODUCT_MANAGER)) 152 | .addAllRepeatedPerson( 153 | listOf( 154 | Person.newBuilder().setName("John").setAge(30).build(), 155 | Person.newBuilder().setName("Jane").setAge(25).build() 156 | ) 157 | ) 158 | .build() 159 | val kroto = proto.toDataClass() 160 | Assertions.assertThat(kroto.repeatedString).containsExactly("1", "2", "3") 161 | Assertions.assertThat(kroto.repeatedJob).containsExactly(Job.ENGINEER, Job.PRODUCT_MANAGER) 162 | Assertions.assertThat(kroto.repeatedPerson).containsExactly( 163 | io.github.mscheong01.test.krotodc.Person(name = "John", age = 30), 164 | io.github.mscheong01.test.krotodc.Person(name = "Jane", age = 25) 165 | ) 166 | val proto2 = kroto.toProto() 167 | Assertions.assertThat(proto2.repeatedStringList).containsExactly("1", "2", "3") 168 | Assertions.assertThat(proto2.repeatedJobList).containsExactly(Job.ENGINEER, Job.PRODUCT_MANAGER) 169 | Assertions.assertThat(proto2.repeatedPersonList).containsExactly( 170 | Person.newBuilder().setName("John").setAge(30).build(), 171 | Person.newBuilder().setName("Jane").setAge(25).build() 172 | ) 173 | Assertions.assertThat(proto2).isEqualTo(proto) 174 | } 175 | 176 | @Test 177 | fun `test message with map fields`() { 178 | val proto = MapMessage.newBuilder() 179 | .putAllMapStringString(mapOf("1" to "2", "3" to "4")) 180 | .putAllMapStringPerson(mapOf("5" to Person.newBuilder().setName("John").setAge(30).build())) 181 | .putAllMapIntJob(mapOf(6 to Job.ENGINEER, 7 to Job.PRODUCT_MANAGER)) 182 | .build() 183 | val kroto = proto.toDataClass() 184 | Assertions.assertThat(kroto.mapStringString).contains(entry("1", "2"), entry("3", "4")) 185 | Assertions.assertThat(kroto.mapStringPerson).contains( 186 | entry("5", io.github.mscheong01.test.krotodc.Person(name = "John", age = 30)) 187 | ) 188 | Assertions.assertThat(kroto.mapIntJob) 189 | .contains(entry(6, Job.ENGINEER), entry(7, Job.PRODUCT_MANAGER)) 190 | val proto2 = kroto.toProto() 191 | Assertions.assertThat(proto2).isEqualTo(proto) 192 | } 193 | 194 | @Test 195 | fun `test message with optional fields`() { 196 | val proto = OptionalMessage.newBuilder() 197 | .setOptionalInt(1) 198 | .setOptionalPerson(Person.newBuilder().setName("John").setAge(30).build()) 199 | .setOptionalString("2") 200 | .build() 201 | val kroto = proto.toDataClass() 202 | Assertions.assertThat(kroto.optionalInt).isEqualTo(1) 203 | Assertions.assertThat(kroto.optionalPerson).isEqualTo( 204 | io.github.mscheong01.test.krotodc.Person(name = "John", age = 30) 205 | ) 206 | Assertions.assertThat(kroto.optionalString).isEqualTo("2") 207 | val proto2 = kroto.toProto() 208 | Assertions.assertThat(proto2.optionalInt).isEqualTo(1) 209 | Assertions.assertThat(proto2.optionalPerson).isEqualTo( 210 | Person.newBuilder().setName("John").setAge(30).build() 211 | ) 212 | Assertions.assertThat(proto2.optionalString).isEqualTo("2") 213 | 214 | val unsetProto = OptionalMessage.newBuilder().build() 215 | val unsetKroto = unsetProto.toDataClass() 216 | Assertions.assertThat(unsetKroto.optionalInt).isNull() 217 | Assertions.assertThat(unsetKroto.optionalPerson).isNull() 218 | Assertions.assertThat(unsetKroto.optionalString).isNull() 219 | Assertions.assertThat(proto2).isEqualTo(proto) 220 | } 221 | 222 | @Test 223 | fun `test message with oneof fields`() { 224 | val personSetOneofProto = OneOfMessage.newBuilder() 225 | .setOneofPerson(Person.newBuilder().setName("John").setAge(30).build()) 226 | .build() 227 | val kroto = personSetOneofProto.toDataClass() 228 | Assertions.assertThat(kroto.oneofField) 229 | .isInstanceOf(io.github.mscheong01.test.krotodc.OneOfMessage.OneofField.OneofPerson::class.java) 230 | Assertions.assertThat( 231 | kroto.oneofField as io.github.mscheong01.test.krotodc.OneOfMessage.OneofField.OneofPerson 232 | ) 233 | .isEqualTo( 234 | io.github.mscheong01.test.krotodc.OneOfMessage.OneofField.OneofPerson( 235 | io.github.mscheong01.test.krotodc.Person(name = "John", age = 30) 236 | ) 237 | ) 238 | val personSetOneofProto2 = kroto.toProto() 239 | Assertions.assertThat(personSetOneofProto2.oneofPerson).isEqualTo( 240 | Person.newBuilder().setName("John").setAge(30).build() 241 | ) 242 | Assertions.assertThat(personSetOneofProto2.oneofFieldCase) 243 | .isEqualTo(OneOfMessage.OneofFieldCase.ONEOF_PERSON) 244 | 245 | val stringSetOneofProto = OneOfMessage.newBuilder() 246 | .setOneofString("1") 247 | .build() 248 | val kroto2 = stringSetOneofProto.toDataClass() 249 | Assertions.assertThat(kroto2.oneofField) 250 | .isInstanceOf(io.github.mscheong01.test.krotodc.OneOfMessage.OneofField.OneofString::class.java) 251 | Assertions.assertThat( 252 | kroto2.oneofField as io.github.mscheong01.test.krotodc.OneOfMessage.OneofField.OneofString 253 | ) 254 | .isEqualTo(io.github.mscheong01.test.krotodc.OneOfMessage.OneofField.OneofString("1")) 255 | val stringSetOneofProto2 = kroto2.toProto() 256 | Assertions.assertThat(stringSetOneofProto2.oneofString).isEqualTo("1") 257 | Assertions.assertThat(stringSetOneofProto2.oneofFieldCase) 258 | .isEqualTo(OneOfMessage.OneofFieldCase.ONEOF_STRING) 259 | 260 | val unsetOneofProto = OneOfMessage.newBuilder().build() 261 | val unsetKroto = unsetOneofProto.toDataClass() 262 | Assertions.assertThat(unsetKroto.oneofField).isNull() 263 | val unsetOneofProto2 = unsetKroto.toProto() 264 | Assertions.assertThat(unsetOneofProto2.oneofFieldCase) 265 | .isEqualTo(OneOfMessage.OneofFieldCase.ONEOFFIELD_NOT_SET) 266 | Assertions.assertThat(unsetOneofProto2).isEqualTo(unsetOneofProto) 267 | } 268 | 269 | @Test 270 | fun `test message without any fields`() { 271 | val proto = EmptyMessage.newBuilder().build() 272 | val kroto = proto.toDataClass() 273 | Assertions.assertThat(kroto).isNotNull 274 | val proto2 = kroto.toProto() 275 | Assertions.assertThat(proto2).isNotNull 276 | Assertions.assertThat(proto2).isEqualTo(proto) 277 | } 278 | 279 | @Test 280 | fun `test message with deprecated fields`() { 281 | val proto = DeprecatedMessage.newBuilder() 282 | .setName("1") 283 | .setOneofString("2") 284 | .build() 285 | val kroto = proto.toDataClass() 286 | Assertions.assertThat(kroto::name.annotations) 287 | .anyMatch { it is Deprecated } 288 | Assertions.assertThat(kroto.oneofField!!::class.annotations) 289 | .anyMatch { it is Deprecated } 290 | } 291 | 292 | @Test 293 | fun `test message with imported type field`() { 294 | val proto = ImportTestMessage.newBuilder() 295 | .setImportedNestedMessage(TopLevelMessage.NestedMessage.newBuilder().setName("test").build()) 296 | .setImportedPerson(Person.newBuilder().setName("John").setAge(30).build()) 297 | .setImportedSimpleMessage(OuterClassNameTestProto.SimpleMessage.newBuilder().setName("test").build()) 298 | .build() 299 | val kroto = proto.toDataClass() 300 | Assertions.assertThat(kroto.importedNestedMessage).isEqualTo( 301 | io.github.mscheong01.test.krotodc.TopLevelMessage.NestedMessage(name = "test") 302 | ) 303 | Assertions.assertThat(kroto.importedPerson).isEqualTo( 304 | io.github.mscheong01.test.krotodc.Person(name = "John", age = 30) 305 | ) 306 | val proto2 = kroto.toProto() 307 | Assertions.assertThat(proto2).isEqualTo(proto) 308 | } 309 | } 310 | --------------------------------------------------------------------------------