diff --git a/settings.gradle b/settings.gradle index 68275320..c62f86f5 100644 --- a/settings.gradle +++ b/settings.gradle @@ -1,6 +1,7 @@ rootProject.name = 'thrifty' include 'thrifty-schema' include 'thrifty-runtime' +include 'thrifty-runtime-ktx' include 'thrifty-java-codegen' include 'thrifty-kotlin-codegen' include 'thrifty-compiler' diff --git a/thrifty-compiler/src/main/kotlin/com/microsoft/thrifty/compiler/ThriftyCompiler.kt b/thrifty-compiler/src/main/kotlin/com/microsoft/thrifty/compiler/ThriftyCompiler.kt index 9696627e..0577b612 100644 --- a/thrifty-compiler/src/main/kotlin/com/microsoft/thrifty/compiler/ThriftyCompiler.kt +++ b/thrifty-compiler/src/main/kotlin/com/microsoft/thrifty/compiler/ThriftyCompiler.kt @@ -129,6 +129,9 @@ class ThriftyCompiler { "--kt-file-per-type", help = "Generate one .kt file per type; default is one per namespace.") .flag(default = false) + val kotlinBuilderlessDataClasses: Boolean by option("--experimental-kt-builderless-structs") + .flag(default = false) + val thriftFiles: List by argument(help = "All .thrift files to compile") .path(exists = true, fileOkay = true, folderOkay = false, readable = true) .multiple() @@ -157,6 +160,7 @@ class ThriftyCompiler { } val impliedLanguage = when { + kotlinBuilderlessDataClasses -> Language.KOTLIN kotlinFilePerType -> Language.KOTLIN emitNullabilityAnnotations -> Language.JAVA else -> null @@ -214,6 +218,10 @@ class ThriftyCompiler { setTypeName?.let { gen.setClassName(it) } mapTypeName?.let { gen.mapClassName(it) } + if (kotlinBuilderlessDataClasses) { + gen.builderlessDataClasses() + } + val svc = TypeProcessorService.getInstance() svc.kotlinProcessor?.let { gen.processor = it diff --git a/thrifty-integration-tests/build.gradle b/thrifty-integration-tests/build.gradle index a999f5e7..aba4dd47 100644 --- a/thrifty-integration-tests/build.gradle +++ b/thrifty-integration-tests/build.gradle @@ -27,6 +27,7 @@ dependencies { implementation project(':thrifty-compiler') testImplementation project(':thrifty-runtime') + testImplementation project(':thrifty-runtime-ktx') testImplementation project(':thrifty-test-server') testImplementation libraries.guava diff --git a/thrifty-kotlin-codegen/build.gradle b/thrifty-kotlin-codegen/build.gradle index 00653fab..f6f9045b 100644 --- a/thrifty-kotlin-codegen/build.gradle +++ b/thrifty-kotlin-codegen/build.gradle @@ -32,6 +32,7 @@ dependencies { api libraries.kotlin implementation project(':thrifty-runtime') + implementation project(':thrifty-runtime-ktx') testImplementation libraries.testing diff --git a/thrifty-kotlin-codegen/src/main/kotlin/com/microsoft/thrifty/kgen/KotlinCodeGenerator.kt b/thrifty-kotlin-codegen/src/main/kotlin/com/microsoft/thrifty/kgen/KotlinCodeGenerator.kt index a9a5bc2a..8854fc27 100644 --- a/thrifty-kotlin-codegen/src/main/kotlin/com/microsoft/thrifty/kgen/KotlinCodeGenerator.kt +++ b/thrifty-kotlin-codegen/src/main/kotlin/com/microsoft/thrifty/kgen/KotlinCodeGenerator.kt @@ -32,11 +32,13 @@ import com.microsoft.thrifty.TType import com.microsoft.thrifty.ThriftException import com.microsoft.thrifty.ThriftField import com.microsoft.thrifty.compiler.spi.KotlinTypeProcessor +import com.microsoft.thrifty.kotlin.Adapter as KtAdapter import com.microsoft.thrifty.protocol.MessageMetadata import com.microsoft.thrifty.protocol.Protocol import com.microsoft.thrifty.schema.BuiltinType import com.microsoft.thrifty.schema.Constant import com.microsoft.thrifty.schema.EnumType +import com.microsoft.thrifty.schema.Field import com.microsoft.thrifty.schema.FieldNamingPolicy import com.microsoft.thrifty.schema.ListType import com.microsoft.thrifty.schema.MapType @@ -102,12 +104,6 @@ private object Tags { /** * Generates Kotlin code from a [Schema]. * - * While substantially complete, there is a bit more yet to be implemented: - * - Services (coroutine-based?) - * - Builderless adapters (builders are dumb, given data classes) - * - Customizable collection types? Some droids prefer ArrayMap, ArraySet, etc - * - Option to emit one file per type - * * @param fieldNamingPolicy A user-specified naming policy for fields. */ class KotlinCodeGenerator( @@ -122,6 +118,7 @@ class KotlinCodeGenerator( private var shouldImplementStruct: Boolean = true private var parcelize: Boolean = false + private var builderlessDataClasses: Boolean = false private var listClassName: ClassName? = null private var setClassName: ClassName? = null @@ -184,6 +181,8 @@ class KotlinCodeGenerator( } }) + // region Configuration + var processor: KotlinTypeProcessor = NoTypeProcessor var outputStyle: OutputStyle = OutputStyle.FILE_PER_NAMESPACE @@ -203,10 +202,16 @@ class KotlinCodeGenerator( this.mapClassName = ClassName.bestGuess(name) } + fun builderlessDataClasses(): KotlinCodeGenerator = apply { + this.builderlessDataClasses = true + } + private object NoTypeProcessor : KotlinTypeProcessor { override fun process(typeSpec: TypeSpec) = typeSpec } + // endregion Configuration + fun generate(schema: Schema): List { TypeSpec.classBuilder("foo") .addModifiers(KModifier.DATA) @@ -289,7 +294,7 @@ class KotlinCodeGenerator( // region Aliases - fun generateTypeAlias(typedef: TypedefType): TypeAliasSpec { + internal fun generateTypeAlias(typedef: TypedefType): TypeAliasSpec { return TypeAliasSpec.builder(typedef.name, typedef.oldType.typeName).run { if (typedef.hasJavadoc) { addKdoc("%L", typedef.documentation) @@ -302,7 +307,7 @@ class KotlinCodeGenerator( // region Enums - fun generateEnumClass(enumType: EnumType): TypeSpec { + internal fun generateEnumClass(enumType: EnumType): TypeSpec { val typeBuilder = TypeSpec.enumBuilder(enumType.name) .addProperty(PropertySpec.builder("value", INT) .jvmField() @@ -356,7 +361,7 @@ class KotlinCodeGenerator( // region Structs - fun generateDataClass(schema: Schema, struct: StructType): TypeSpec { + internal fun generateDataClass(schema: Schema, struct: StructType): TypeSpec { val structClassName = ClassName(struct.kotlinNamespace, struct.name) val typeBuilder = TypeSpec.classBuilder(structClassName).apply { if (struct.fields.isNotEmpty()) { @@ -412,7 +417,7 @@ class KotlinCodeGenerator( typeBuilder.addProperty(prop.build()) } - if (true) { // TODO: Add an option to generate Java-style builders + if (!builderlessDataClasses) { val builderTypeName = ClassName(struct.kotlinNamespace, struct.name, "Builder") val adapterTypeName = ClassName(struct.kotlinNamespace, struct.name, "${struct.name}Adapter") @@ -427,7 +432,17 @@ class KotlinCodeGenerator( .jvmField() .build()) } else { - TODO("Builderless adapters") + val adapterTypeName = ClassName(struct.kotlinNamespace, struct.name, "${struct.name}Adapter") + val adapterInterfaceTypeName = KtAdapter::class + .asTypeName() + .parameterizedBy(struct.typeName) + + typeBuilder.addType(generateAdapterFor(struct, adapterTypeName, adapterInterfaceTypeName, null)) + + companionBuilder.addProperty(PropertySpec.builder("ADAPTER", adapterInterfaceTypeName) + .initializer("%T()", adapterTypeName) + .jvmField() + .build()) } if (struct.fields.any { it.isObfuscated || it.isRedacted } || struct.fields.isEmpty()) { @@ -469,7 +484,7 @@ class KotlinCodeGenerator( // region Redaction/obfuscation - fun generateToString(struct: StructType): FunSpec { + internal fun generateToString(struct: StructType): FunSpec { val block = buildCodeBlock { add("return \"${struct.name}(") @@ -531,7 +546,7 @@ class KotlinCodeGenerator( // region Builders - fun generateBuilderFor(schema: Schema, struct: StructType): TypeSpec { + internal fun generateBuilderFor(schema: Schema, struct: StructType): TypeSpec { val structTypeName = ClassName(struct.kotlinNamespace, struct.name) val spec = TypeSpec.classBuilder("Builder") .addSuperinterface(StructBuilder::class.asTypeName().parameterizedBy(structTypeName)) @@ -617,20 +632,32 @@ class KotlinCodeGenerator( // region Adapters - fun generateAdapterFor( + /** + * Generates an adapter for the given struct type. + * + * The kind of adapter generated depends on whether a [builderType] is + * provided. If so, a conventional [com.microsoft.thrifty.Adapter] gets + * created, making use of the given [builderType]. If not, a so-called + * "builderless" [com.microsoft.thrifty.kotlin.Adapter] is the result. + */ + internal fun generateAdapterFor( struct: StructType, adapterName: ClassName, adapterInterfaceName: TypeName, - builderType: ClassName): TypeSpec { + builderType: ClassName?): TypeSpec { val adapter = TypeSpec.classBuilder(adapterName) .addModifiers(KModifier.PRIVATE) .addSuperinterface(adapterInterfaceName) - val reader = FunSpec.builder("read") - .addModifiers(KModifier.OVERRIDE) - .returns(struct.typeName) - .addParameter("protocol", Protocol::class) - .addParameter("builder", builderType) + val reader = FunSpec.builder("read").apply { + addModifiers(KModifier.OVERRIDE) + returns(struct.typeName) + addParameter("protocol", Protocol::class) + + if (builderType != null) { + addParameter("builder", builderType) + } + } val writer = FunSpec.builder("write") .addModifiers(KModifier.OVERRIDE) @@ -669,6 +696,16 @@ class KotlinCodeGenerator( // Reader next + fun localFieldName(field: Field): String { + return "_local_${field.name}" + } + + if (builderType == null) { + for (field in struct.fields) { + reader.addStatement("var %N: %T? = null", localFieldName(field), field.type.typeName) + } + } + reader.addStatement("protocol.readStructBegin()") reader.beginControlFlow("while (true)") @@ -691,7 +728,12 @@ class KotlinCodeGenerator( beginControlFlow("if (fieldMeta.typeId == %T.%L)", TType::class, fieldType.typeCodeName) generateRecursiveReadCall(this, name, fieldType) - addStatement("builder.$name($name)") + + if (builderType != null) { + addStatement("builder.$name($name)") + } else { + addStatement("%N = $name", localFieldName(field)) + } nextControlFlow("else") addStatement("%T.skip(protocol, fieldMeta.typeId)", ProtocolUtil::class) @@ -709,16 +751,52 @@ class KotlinCodeGenerator( reader.addStatement("protocol.readFieldEnd()") reader.endControlFlow() // while (true) reader.addStatement("protocol.readStructEnd()") - reader.addStatement("return builder.build()") + + if (builderType != null) { + reader.addStatement("return builder.build()") + } else { + val block = CodeBlock.builder() + block.add("%[return %T(", struct.typeName) + + val hasRequiredField = struct.fields.any { it.required } + val newlinePerParam = (hasRequiredField && struct.fields.size > 1) || struct.fields.size > 2 + val separator = if (newlinePerParam) System.lineSeparator() else "%W" + + if (newlinePerParam) { + block.add(System.lineSeparator()) + } + + for ((ix, field) in struct.fields.withIndex()) { + if (ix > 0) { + block.add(",%L", separator) + } + + block.add("%N = ", nameAllocator.get(field)) + if (field.required) { + block.add("checkNotNull(%N) { %S }", + localFieldName(field), + "Required field '${nameAllocator.get(field)}' is missing") + } else { + block.add("%N", localFieldName(field)) + } + } + + block.add(")%]%L", System.lineSeparator()) + + reader.addCode(block.build()) + } + + if (builderType != null) { + adapter.addFunction(FunSpec.builder("read") + .addModifiers(KModifier.OVERRIDE) + .addParameter("protocol", Protocol::class) + .addStatement("return read(protocol, %T())", builderType) + .build()) + } return adapter - .addFunction(writer.build()) .addFunction(reader.build()) - .addFunction(FunSpec.builder("read") - .addModifiers(KModifier.OVERRIDE) - .addParameter("protocol", Protocol::class) - .addStatement("return read(protocol, %T())", builderType) - .build()) + .addFunction(writer.build()) .build() } @@ -974,7 +1052,7 @@ class KotlinCodeGenerator( // region Constants - fun generateConstantProperty(schema: Schema, allocator: NameAllocator, constant: Constant): PropertySpec { + internal fun generateConstantProperty(schema: Schema, allocator: NameAllocator, constant: Constant): PropertySpec { val type = constant.type val typeName = type.typeName val propName = allocator.newName(constant.name, constant) @@ -1025,7 +1103,7 @@ class KotlinCodeGenerator( return propBuilder.build() } - fun renderConstValue(schema: Schema, thriftType: ThriftType, valueElement: ConstValueElement): CodeBlock { + internal fun renderConstValue(schema: Schema, thriftType: ThriftType, valueElement: ConstValueElement): CodeBlock { fun recursivelyRenderConstValue(block: CodeBlock.Builder, type: ThriftType, value: ConstValueElement) { type.accept(object : ThriftType.Visitor { override fun visitVoid(voidType: BuiltinType) { @@ -1299,7 +1377,7 @@ class KotlinCodeGenerator( // region Services - fun generateServiceInterface(serviceType: ServiceType): TypeSpec { + internal fun generateServiceInterface(serviceType: ServiceType): TypeSpec { val type = TypeSpec.interfaceBuilder(serviceType.name).apply { if (serviceType.hasJavadoc) addKdoc("%L", serviceType.documentation) if (serviceType.isDeprecated) addAnnotation(makeDeprecated()) @@ -1342,7 +1420,7 @@ class KotlinCodeGenerator( return type.build() } - fun generateServiceImplementation(schema: Schema, serviceType: ServiceType, serviceInterface: TypeSpec): TypeSpec { + internal fun generateServiceImplementation(schema: Schema, serviceType: ServiceType, serviceInterface: TypeSpec): TypeSpec { val type = TypeSpec.classBuilder(serviceType.name + "Client").apply { val baseType = serviceType.extendsService as? ServiceType val baseClassName = if (baseType != null) { diff --git a/thrifty-runtime-ktx/build.gradle b/thrifty-runtime-ktx/build.gradle new file mode 100644 index 00000000..bd30b828 --- /dev/null +++ b/thrifty-runtime-ktx/build.gradle @@ -0,0 +1,28 @@ +/* + * Thrifty + * + * Copyright (c) Microsoft Corporation + * + * All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the License); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * THIS CODE IS PROVIDED ON AN *AS IS* BASIS, WITHOUT WARRANTIES OR + * CONDITIONS OF ANY KIND, EITHER EXPRESS OR IMPLIED, INCLUDING + * WITHOUT LIMITATION ANY IMPLIED WARRANTIES OR CONDITIONS OF TITLE, + * FITNESS FOR A PARTICULAR PURPOSE, MERCHANTABLITY OR NON-INFRINGEMENT. + * + * See the Apache Version 2.0 License for specific language governing permissions and limitations under the License. + */ +description = 'Provides Kotlin support and compatibility with thrifty-runtime.' + +apply plugin: 'kotlin' + +dependencies { + api project(':thrifty-runtime') + api libraries.kotlin +} \ No newline at end of file diff --git a/thrifty-runtime-ktx/src/main/kotlin/com/microsoft/thrifty/kotlin/Adapter.kt b/thrifty-runtime-ktx/src/main/kotlin/com/microsoft/thrifty/kotlin/Adapter.kt new file mode 100644 index 00000000..eba9212d --- /dev/null +++ b/thrifty-runtime-ktx/src/main/kotlin/com/microsoft/thrifty/kotlin/Adapter.kt @@ -0,0 +1,46 @@ +/* + * Thrifty + * + * Copyright (c) Microsoft Corporation + * + * All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the License); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * THIS CODE IS PROVIDED ON AN *AS IS* BASIS, WITHOUT WARRANTIES OR + * CONDITIONS OF ANY KIND, EITHER EXPRESS OR IMPLIED, INCLUDING + * WITHOUT LIMITATION ANY IMPLIED WARRANTIES OR CONDITIONS OF TITLE, + * FITNESS FOR A PARTICULAR PURPOSE, MERCHANTABLITY OR NON-INFRINGEMENT. + * + * See the Apache Version 2.0 License for specific language governing permissions and limitations under the License. + */ +package com.microsoft.thrifty.kotlin + +import com.microsoft.thrifty.protocol.Protocol + +/** + * An object that can read and write Kotlin data classes generated + * by the Thrifty compiler, to and from a [Protocol] object. + * + * @param T The struct type, generated by the Thrifty compiler. + */ +interface Adapter { + /** + * Reads a Thrift struct from the given [protocol]. + * + * @param protocol A [Protocol] from which to read a struct. + */ + fun read(protocol: Protocol): T + + /** + * Writes the given [struct] to the given [protocol]. + * + * @param protocol A [Protocol] to which to write the [struct]. + * @param struct A struct to be written out. + */ + fun write(protocol: Protocol, struct: T) +} \ No newline at end of file