diff --git a/kotlin-codegen/compiler/pom.xml b/kotlin-codegen/compiler/pom.xml new file mode 100644 index 0000000..d6aa8dd --- /dev/null +++ b/kotlin-codegen/compiler/pom.xml @@ -0,0 +1,134 @@ + + + + 4.0.0 + + + com.squareup.moshi + moshi-parent + 1.6.0-SNAPSHOT + ../../pom.xml + + + moshi-kotlin-codegen-compiler + + + + jcenter + https://jcenter.bintray.com/ + + + + + + com.squareup.moshi + moshi + ${project.version} + + + com.squareup.moshi + moshi-kotlin-codegen-runtime + ${project.version} + + + org.jetbrains.kotlin + kotlin-stdlib + + + me.eugeniomarletti + kotlin-metadata + 1.2.1 + + + com.google.auto + auto-common + 0.10 + + + com.google.auto.service + auto-service + 1.0-rc4 + provided + + + com.squareup + kotlinpoet + 0.7.0 + + + junit + junit + test + + + org.assertj + assertj-core + test + + + + + + + org.jetbrains.kotlin + kotlin-maven-plugin + ${kotlin.version} + + + kapt + + kapt + + + + src/main/kotlin + src/main/java + + + + com.google.auto.service + auto-service + 1.0-rc4 + + + + + + compile + compile + + compile + + + + test-compile + test-compile + + test-compile + + + + + + org.apache.maven.plugins + maven-compiler-plugin + + + compile + compile + + compile + + + + testCompile + test-compile + + testCompile + + + + + + + diff --git a/kotlin-codegen/compiler/src/main/java/com/squareup/moshi/MoshiKotlinCodeGenProcessor.kt b/kotlin-codegen/compiler/src/main/java/com/squareup/moshi/MoshiKotlinCodeGenProcessor.kt new file mode 100644 index 0000000..c44c94b --- /dev/null +++ b/kotlin-codegen/compiler/src/main/java/com/squareup/moshi/MoshiKotlinCodeGenProcessor.kt @@ -0,0 +1,764 @@ +/* + * Copyright (C) 2018 Square, Inc. + * + * 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 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.squareup.moshi + +import com.google.auto.common.AnnotationMirrors +import com.google.auto.service.AutoService +import com.squareup.kotlinpoet.ANY +import com.squareup.kotlinpoet.ARRAY +import com.squareup.kotlinpoet.BOOLEAN +import com.squareup.kotlinpoet.BYTE +import com.squareup.kotlinpoet.CHAR +import com.squareup.kotlinpoet.ClassName +import com.squareup.kotlinpoet.CodeBlock +import com.squareup.kotlinpoet.DOUBLE +import com.squareup.kotlinpoet.FLOAT +import com.squareup.kotlinpoet.FileSpec +import com.squareup.kotlinpoet.FunSpec +import com.squareup.kotlinpoet.INT +import com.squareup.kotlinpoet.KModifier +import com.squareup.kotlinpoet.KModifier.IN +import com.squareup.kotlinpoet.KModifier.OUT +import com.squareup.kotlinpoet.KModifier.OVERRIDE +import com.squareup.kotlinpoet.KModifier.PRIVATE +import com.squareup.kotlinpoet.LONG +import com.squareup.kotlinpoet.NameAllocator +import com.squareup.kotlinpoet.ParameterSpec +import com.squareup.kotlinpoet.ParameterizedTypeName +import com.squareup.kotlinpoet.PropertySpec +import com.squareup.kotlinpoet.SHORT +import com.squareup.kotlinpoet.TypeName +import com.squareup.kotlinpoet.TypeSpec +import com.squareup.kotlinpoet.TypeVariableName +import com.squareup.kotlinpoet.WildcardTypeName +import com.squareup.kotlinpoet.asClassName +import com.squareup.kotlinpoet.asTypeName +import me.eugeniomarletti.kotlin.metadata.KotlinClassMetadata +import me.eugeniomarletti.kotlin.metadata.KotlinMetadataUtils +import me.eugeniomarletti.kotlin.metadata.declaresDefaultValue +import me.eugeniomarletti.kotlin.metadata.extractFullName +import me.eugeniomarletti.kotlin.metadata.isDataClass +import me.eugeniomarletti.kotlin.metadata.isPrimary +import me.eugeniomarletti.kotlin.metadata.jvm.getJvmConstructorSignature +import me.eugeniomarletti.kotlin.metadata.kotlinMetadata +import me.eugeniomarletti.kotlin.metadata.visibility +import me.eugeniomarletti.kotlin.processing.KotlinAbstractProcessor +import org.jetbrains.kotlin.serialization.ProtoBuf +import org.jetbrains.kotlin.serialization.ProtoBuf.Type.Argument.Projection +import org.jetbrains.kotlin.serialization.ProtoBuf.TypeParameter.Variance +import org.jetbrains.kotlin.serialization.ProtoBuf.Visibility +import org.jetbrains.kotlin.serialization.ProtoBuf.Visibility.INTERNAL +import org.jetbrains.kotlin.serialization.deserialization.NameResolver +import java.io.File +import java.lang.reflect.Type +import javax.annotation.processing.Processor +import javax.annotation.processing.RoundEnvironment +import javax.lang.model.SourceVersion +import javax.lang.model.element.AnnotationMirror +import javax.lang.model.element.Element +import javax.lang.model.element.ElementKind +import javax.lang.model.element.ExecutableElement +import javax.lang.model.element.TypeElement +import javax.lang.model.util.Elements +import javax.tools.Diagnostic.Kind.ERROR + +/** + * An annotation processor that reads Kotlin data classes and generates Moshi JsonAdapters for them. + * This generates Kotlin code, and understands basic Kotlin language features like default values + * and companion objects. + * + * The generated class will match the visibility of the given data class (i.e. if it's internal, the + * adapter will also be internal). + * + * If you define a companion object, a jsonAdapter() extension function will be generated onto it. + * If you don't want this though, you can use the runtime [MoshiSerializable] factory implementation. + */ +@AutoService(Processor::class) +class MoshiKotlinCodeGenProcessor : KotlinAbstractProcessor(), KotlinMetadataUtils { + + private val annotationName = MoshiSerializable::class.java.canonicalName + + override fun getSupportedAnnotationTypes() = setOf(annotationName) + + override fun getSupportedSourceVersion(): SourceVersion = SourceVersion.latest() + + override fun process(annotations: Set, roundEnv: RoundEnvironment): Boolean { + val annotationElement = elementUtils.getTypeElement(annotationName) + roundEnv.getElementsAnnotatedWith(annotationElement) + .asSequence() + .mapNotNull { processElement(it) } + .forEach { it.generateAndWrite() } + + return true + } + + private fun processElement(element: Element): Adapter? { + val metadata = element.kotlinMetadata + + if (metadata !is KotlinClassMetadata) { + errorMustBeDataClass(element) + return null + } + + val classData = metadata.data + val (nameResolver, classProto) = classData + + fun ProtoBuf.Type.extractFullName() = extractFullName(classData) + + if (!classProto.isDataClass) { + errorMustBeDataClass(element) + return null + } + + val fqClassName = nameResolver.getString(classProto.fqName).replace('/', '.') + + val packageName = nameResolver.getString(classProto.fqName).substringBeforeLast('/').replace( + '/', '.') + + val hasCompanionObject = classProto.hasCompanionObjectName() + // todo allow custom constructor + val protoConstructor = classProto.constructorList + .single { it.isPrimary } + val constructorJvmSignature = protoConstructor.getJvmConstructorSignature(nameResolver, + classProto.typeTable) + val constructor = classProto.fqName + .let(nameResolver::getString) + .replace('/', '.') + .let(elementUtils::getTypeElement) + .enclosedElements + .mapNotNull { it.takeIf { it.kind == ElementKind.CONSTRUCTOR }?.let { it as ExecutableElement } } + .first() + // TODO Temporary until jvm method signature matching is better +// .single { it.jvmMethodSignature == constructorJvmSignature } + val parameters = protoConstructor + .valueParameterList + .mapIndexed { index, valueParameter -> + val paramName = nameResolver.getString(valueParameter.name) + + val nullable = valueParameter.type.nullable + val paramFqcn = valueParameter.type.extractFullName() + .replace("`", "") + .removeSuffix("?") + + val actualElement = constructor.parameters[index] + + val serializedName = actualElement.getAnnotation(Json::class.java)?.name + ?: paramName + + val jsonQualifiers = AnnotationMirrors.getAnnotatedAnnotations(actualElement, + JsonQualifier::class.java) + + Property( + name = paramName, + fqClassName = paramFqcn, + serializedName = serializedName, + hasDefault = valueParameter.declaresDefaultValue, + nullable = nullable, + typeName = valueParameter.type.asTypeName(nameResolver, classProto::getTypeParameter), + unaliasedName = valueParameter.type.asTypeName(nameResolver, classProto::getTypeParameter, true), + jsonQualifiers = jsonQualifiers) + } + + val genericTypeNames = classProto.typeParameterList + .map { + val variance = it.variance.asKModifier().let { + // We don't redeclare out variance here + if (it == OUT) { + null + } else { + it + } + } + TypeVariableName.invoke( + name = nameResolver.getString(it.name), + bounds = *(it.upperBoundList + .map { it.asTypeName(nameResolver, classProto::getTypeParameter) } + .toTypedArray()), + variance = variance) + .reified(it.reified) + }.let { + if (it.isEmpty()) { + null + } else { + it + } + } + + return Adapter( + fqClassName = fqClassName, + packageName = packageName, + propertyList = parameters, + originalElement = element, + hasCompanionObject = hasCompanionObject, + visibility = classProto.visibility!!, + genericTypeNames = genericTypeNames, + elementUtils = elementUtils) + } + + private fun errorMustBeDataClass(element: Element) { + messager.printMessage(ERROR, + "@${MoshiSerializable::class.java.simpleName} can't be applied to $element: must be a Kotlin data class", + element) + } + + private fun Adapter.generateAndWrite() { + val adapterName = "${name}JsonAdapter" + val outputDir = generatedDir ?: mavenGeneratedDir(adapterName) + val fileBuilder = FileSpec.builder(packageName, adapterName) + generate(adapterName, fileBuilder) + fileBuilder + .build() + .writeTo(outputDir) + } + + private fun mavenGeneratedDir(adapterName: String): File { + // Hack since the maven plugin doesn't supply `kapt.kotlin.generated` option + // Bug filed at https://youtrack.jetbrains.com/issue/KT-22783 + val file = filer.createSourceFile(adapterName).toUri().let(::File) + return file.parentFile.also { file.delete() } + } +} + +/** + * Creates a joined string representation of simplified typename names. + */ +private fun List.simplifiedNames(): String { + return joinToString("_") { it.simplifiedName() } +} + +private fun TypeName.resolveRawType(): ClassName { + return when (this) { + is ClassName -> this + is ParameterizedTypeName -> rawType + else -> throw IllegalArgumentException("Cannot get raw type from $this") + } +} + +/** + * Creates a simplified string representation of a TypeName's name + */ +private fun TypeName.simplifiedName(): String { + return when (this) { + is ClassName -> simpleName().decapitalize() + is ParameterizedTypeName -> { + rawType.simpleName().decapitalize() + if (typeArguments.isEmpty()) "" else "__" + typeArguments.simplifiedNames() + } + is WildcardTypeName -> "wildcard__" + (lowerBounds + upperBounds).simplifiedNames() + is TypeVariableName -> name.decapitalize() + if (bounds.isEmpty()) "" else "__" + bounds.simplifiedNames() + else -> throw IllegalArgumentException("Unrecognized type! $this") + }.let { if (nullable) "${it}_nullable" else it } +} + +private fun ClassName.isClass(elementUtils: Elements): Boolean { + val fqcn = toString() + if (fqcn.startsWith("kotlin.collections.")) { + // These are special kotlin interfaces are only visible in kotlin, because they're replaced by + // the compiler with concrete java classes + return false + } else if (this == ARRAY) { + // This is a "fake" class and not visible to Elements + return true + } + return elementUtils.getTypeElement(fqcn).kind == ElementKind.INTERFACE +} + +private fun TypeName.objectType(): TypeName { + return when (this) { + BOOLEAN -> Boolean::class.javaObjectType.asTypeName() + BYTE -> Byte::class.javaObjectType.asTypeName() + SHORT -> Short::class.javaObjectType.asTypeName() + INT -> Integer::class.javaObjectType.asTypeName() + LONG -> Long::class.javaObjectType.asTypeName() + CHAR -> Character::class.javaObjectType.asTypeName() + FLOAT -> Float::class.javaObjectType.asTypeName() + DOUBLE -> Double::class.javaObjectType.asTypeName() + else -> this + } +} + +private fun TypeName.makeType( + elementUtils: Elements, + typesArray: ParameterSpec, + genericTypeNames: List): CodeBlock { + if (nullable) { + return asNonNullable().makeType(elementUtils, typesArray, genericTypeNames) + } + return when (this) { + is ClassName -> CodeBlock.of("%T::class.java", this) + is ParameterizedTypeName -> { + // If it's an Array type, we shortcut this to return Types.arrayOf() + if (rawType == ARRAY) { + return CodeBlock.of("%T.arrayOf(%L)", + Types::class, + typeArguments[0].objectType().makeType(elementUtils, typesArray, genericTypeNames)) + } + // If it's a Class type, we have to specify the generics. + val rawTypeParameters = if (rawType.isClass(elementUtils)) { + CodeBlock.of( + typeArguments.joinTo( + buffer = StringBuilder(), + separator = ", ", + prefix = "<", + postfix = ">") { "%T" } + .toString(), + *(typeArguments.map { objectType() }.toTypedArray()) + ) + } else { + CodeBlock.of("") + } + CodeBlock.of( + "%T.newParameterizedType(%T%L::class.java, ${typeArguments + .joinToString(", ") { "%L" }})", + Types::class, + rawType.objectType(), + rawTypeParameters, + *(typeArguments.map { + it.objectType().makeType(elementUtils, typesArray, genericTypeNames) + }.toTypedArray())) + } + is WildcardTypeName -> { + val target: TypeName + val method: String + when { + lowerBounds.size == 1 -> { + target = lowerBounds[0] + method = "supertypeOf" + } + upperBounds.size == 1 -> { + target = upperBounds[0] + method = "subtypeOf" + } + else -> throw IllegalArgumentException( + "Unrepresentable wildcard type. Cannot have more than one bound: " + this) + } + CodeBlock.of("%T.%L(%T::class.java)", Types::class, method, target) + } + is TypeVariableName -> { + CodeBlock.of("%N[%L]", typesArray, genericTypeNames.indexOfFirst { it == this }) + } + else -> throw IllegalArgumentException("Unrepresentable type: " + this) + } +} + +private data class Property( + val name: String, + val fqClassName: String, + val serializedName: String, + val hasDefault: Boolean, + val nullable: Boolean, + val typeName: TypeName, + val unaliasedName: TypeName, + val jsonQualifiers: Set) { + + val isRequired = !nullable && !hasDefault +} + +private data class Adapter( + val fqClassName: String, + val packageName: String, + val propertyList: List, + val originalElement: Element, + val name: String = fqClassName.substringAfter(packageName) + .replace('.', '_') + .removePrefix("_"), + val hasCompanionObject: Boolean, + val visibility: Visibility, + val elementUtils: Elements, + val genericTypeNames: List?) { + + fun generate(adapterName: String, fileSpecBuilder: FileSpec.Builder) { + val nameAllocator = NameAllocator() + fun String.allocate() = nameAllocator.newName(this) + + val originalTypeName = originalElement.asType().asTypeName() + val moshiName = "moshi".allocate() + val moshiParam = ParameterSpec.builder(moshiName, Moshi::class).build() + val typesParam = ParameterSpec.builder("types".allocate(), + ParameterizedTypeName.get(ARRAY, Type::class.asTypeName())).build() + val reader = ParameterSpec.builder("reader".allocate(), + JsonReader::class).build() + val writer = ParameterSpec.builder("writer".allocate(), + JsonWriter::class).build() + val value = ParameterSpec.builder("value".allocate(), + originalTypeName.asNullable()).build() + val jsonAdapterTypeName = ParameterizedTypeName.get(JsonAdapter::class.asClassName(), + originalTypeName) + + // Create fields + val adapterProperties = propertyList + .distinctBy { it.unaliasedName to it.jsonQualifiers } + .associate { prop -> + val typeName = prop.unaliasedName + val qualifierNames = prop.jsonQualifiers.joinToString("") { + "at${it.annotationType.asElement().simpleName.toString().capitalize()}" + } + val propertyName = typeName.simplifiedName().allocate().let { + if (qualifierNames.isBlank()) { + it + } else { + "$it$qualifierNames" + } + }.let { "${it}Adapter" } + val adapterTypeName = ParameterizedTypeName.get(JsonAdapter::class.asTypeName(), typeName) + val key = typeName to prop.jsonQualifiers + return@associate key to PropertySpec.builder(propertyName, adapterTypeName, PRIVATE) + .apply { + val qualifiers = prop.jsonQualifiers.toList() + val standardArgs = arrayOf(moshiParam, + if (typeName is ClassName && qualifiers.isEmpty()) { + "" + } else { + CodeBlock.of("<%T>", + typeName) + }, + typeName.makeType(elementUtils, typesParam, genericTypeNames ?: emptyList())) + val standardArgsSize = standardArgs.size + 1 + val (initializerString, args) = when { + qualifiers.isEmpty() -> "" to emptyArray() + qualifiers.size == 1 -> { + ", %${standardArgsSize}T::class.java" to arrayOf( + qualifiers.first().annotationType.asTypeName()) + } + else -> { + val initString = qualifiers + .mapIndexed { index, _ -> + val annoClassIndex = standardArgsSize + index + return@mapIndexed "%${annoClassIndex}T::class.java" + } + .joinToString() + val initArgs = qualifiers + .map { it.annotationType.asTypeName() } + .toTypedArray() + ", $initString" to initArgs + } + } + val finalArgs = arrayOf(*standardArgs, *args) + initializer( + "%1N.adapter%2L(%3L$initializerString)${if (prop.nullable) ".nullSafe()" else ""}", + *finalArgs) + } + .build() + } + + val localProperties = + propertyList.associate { prop -> + val propertySpec = PropertySpec.builder(prop.name.allocate(), prop.typeName.asNullable()) + .mutable(true) + .initializer("null") + .build() + val propertySetSpec = if (prop.hasDefault && prop.nullable) { + PropertySpec.builder("${propertySpec.name}Set".allocate(), BOOLEAN) + .mutable(true) + .initializer("false") + .build() + } else { + null + } + val specs = propertySpec to propertySetSpec + prop to specs + } + val optionsByIndex = propertyList + .associateBy { it.serializedName }.entries.withIndex() + + // selectName() API setup + val optionsCN = JsonReader.Options::class.asTypeName() + val optionsProperty = PropertySpec.builder( + "options".allocate(), + optionsCN, + PRIVATE) + .initializer("%T.of(${optionsByIndex.map { it.value.key } + .joinToString(", ") { "\"$it\"" }})", + optionsCN) + .build() + + val adapter = TypeSpec.classBuilder(adapterName) + .superclass(jsonAdapterTypeName) + .apply { + genericTypeNames?.let { + addTypeVariables(genericTypeNames) + } + } + .apply { + // TODO make this configurable. Right now it just matches the source model + if (visibility == INTERNAL) { + addModifiers(KModifier.INTERNAL) + } + } + .primaryConstructor(FunSpec.constructorBuilder() + .addParameter(moshiParam) + .apply { + genericTypeNames?.let { + addParameter(typesParam) + } + } + .build()) + .addProperty(optionsProperty) + .addProperties(adapterProperties.values) + .addFunction(FunSpec.builder("toString") + .addModifiers(OVERRIDE) + .returns(String::class) + .addStatement("return %S", + "GeneratedJsonAdapter(${originalTypeName.resolveRawType() + .simpleNames() + .joinToString(".")})") + .build()) + .addFunction(FunSpec.builder("fromJson") + .addModifiers(OVERRIDE) + .addParameter(reader) + .returns(originalTypeName) + .apply { + localProperties.values.forEach { + addCode("%L", it.first) + it.second?.let { + addCode("%L", it) + } + } + } + .addStatement("%N.beginObject()", reader) + .beginControlFlow("while (%N.hasNext())", reader) + .beginControlFlow("when (%N.selectName(%N))", reader, optionsProperty) + .apply { + optionsByIndex.map { (index, entry) -> index to entry.value } + .forEach { (index, prop) -> + val specs = localProperties[prop]!! + val spec = specs.first + val setterSpec = specs.second + if (setterSpec != null) { + beginControlFlow("%L -> ", index) + addStatement("%N = %N.fromJson(%N)", + spec, + adapterProperties[prop.unaliasedName to prop.jsonQualifiers]!!, + reader) + addStatement("%N = true", setterSpec) + endControlFlow() + } else { + addStatement("%L -> %N = %N.fromJson(%N)", + index, + spec, + adapterProperties[prop.unaliasedName to prop.jsonQualifiers]!!, + reader) + } + } + } + .beginControlFlow("-1 ->") + .addComment("Unknown name, skip it.") + .addStatement("%N.nextName()", reader) + .addStatement("%N.skipValue()", reader) + .endControlFlow() + .endControlFlow() + .endControlFlow() + .addStatement("%N.endObject()", reader) + .apply { + val propertiesWithDefaults = localProperties.entries.filter { it.key.hasDefault } + val propertiesWithoutDefaults = localProperties.entries.filter { !it.key.hasDefault } + val requiredPropertiesCodeBlock = CodeBlock.of( + propertiesWithoutDefaults.joinToString(",\n") { (property, specs) -> + val spec = specs.first + "${property.name} = ${spec.name}%L" + }, + *(propertiesWithoutDefaults + .map { (property, _) -> + if (property.isRequired) { + @Suppress("IMPLICIT_CAST_TO_ANY") + CodeBlock.of( + " ?: throw %T(\"Required property '%L' missing at \${%N.path}\")", + JsonDataException::class, + property.name, + reader + ) + } else { + @Suppress("IMPLICIT_CAST_TO_ANY") + "" + } + } + .toTypedArray())) + if (propertiesWithDefaults.isEmpty()) { + addStatement("return %T(%L)", + originalTypeName, + requiredPropertiesCodeBlock) + } else { + addStatement("return %T(%L)\n.let {\n it.copy(%L)\n}", + originalTypeName, + requiredPropertiesCodeBlock, + propertiesWithDefaults + .joinToString(",\n ") { (property, specs) -> + val spec = specs.first + val setSpec = specs.second + if (setSpec != null) { + "${property.name} = if (${setSpec.name}) ${spec.name} else it.${property.name}" + } else { + "${property.name} = ${spec.name} ?: it.${property.name}" + } + }) + } + } + .build()) + .addFunction(FunSpec.builder("toJson") + .addModifiers(OVERRIDE) + .addParameter(writer) + .addParameter(value) + .beginControlFlow("if (%N == null)", value) + .addStatement("throw %T(%S)", NullPointerException::class, "${value.name} was null! Wrap in .nullSafe() to write nullable values.") + .endControlFlow() + .addStatement("%N.beginObject()", writer) + .apply { + propertyList.forEach { prop -> + addStatement("%N.name(%S)", + writer, + prop.serializedName) + addStatement("%N.toJson(%N, %N.%L)", + adapterProperties[prop.unaliasedName to prop.jsonQualifiers]!!, + writer, + value, + prop.name) + } + } + .addStatement("%N.endObject()", writer) + .build()) + .build() + + if (hasCompanionObject) { + val rawType = when (originalTypeName) { + is TypeVariableName -> throw IllegalArgumentException( + "Cannot get raw type of TypeVariable!") + is ParameterizedTypeName -> originalTypeName.rawType + else -> originalTypeName as ClassName + } + fileSpecBuilder.addFunction(FunSpec.builder("jsonAdapter") + .apply { + // TODO make this configurable. Right now it just matches the source model + if (visibility == INTERNAL) { + addModifiers(KModifier.INTERNAL) + } + } + .receiver(rawType.nestedClass("Companion")) + .returns(jsonAdapterTypeName) + .addParameter(moshiParam) + .apply { + genericTypeNames?.let { + addParameter(typesParam) + addTypeVariables(it) + } + } + .apply { + if (genericTypeNames != null) { + addStatement("return %N(%N, %N)", adapter, moshiParam, typesParam) + } else { + addStatement("return %N(%N)", adapter, moshiParam) + } + } + .build()) + } + fileSpecBuilder.addType(adapter) + } +} + +private fun ProtoBuf.TypeParameter.asTypeName( + nameResolver: NameResolver, + getTypeParameter: (index: Int) -> ProtoBuf.TypeParameter, + resolveAliases: Boolean = false): TypeName { + return TypeVariableName( + name = nameResolver.getString(name), + bounds = *(upperBoundList.map { it.asTypeName(nameResolver, getTypeParameter, resolveAliases) } + .toTypedArray()), + variance = variance.asKModifier() + ) +} + +private fun ProtoBuf.TypeParameter.Variance.asKModifier(): KModifier? { + return when (this) { + Variance.IN -> IN + Variance.OUT -> OUT + Variance.INV -> null + } +} + +/** + * Returns the TypeName of this type as it would be seen in the source code, + * including nullability and generic type parameters. + * + * @param [nameResolver] a [NameResolver] instance from the source proto + * @param [getTypeParameter] + * A function that returns the type parameter for the given index. + * **Only called if [ProtoBuf.Type.hasTypeParameter] is `true`!** + */ +private fun ProtoBuf.Type.asTypeName( + nameResolver: NameResolver, + getTypeParameter: (index: Int) -> ProtoBuf.TypeParameter, + resolveAliases: Boolean = false +): TypeName { + + val argumentList = when { + hasAbbreviatedType() -> abbreviatedType.argumentList + else -> argumentList + } + + if (hasFlexibleUpperBound()) { + return WildcardTypeName.subtypeOf( + flexibleUpperBound.asTypeName(nameResolver, getTypeParameter, resolveAliases)) + } else if (hasOuterType()) { + return WildcardTypeName.supertypeOf(outerType.asTypeName(nameResolver, getTypeParameter, resolveAliases)) + } + + val realType = when { + hasTypeParameter() -> return getTypeParameter(typeParameter) + .asTypeName(nameResolver, getTypeParameter, resolveAliases) + hasTypeParameterName() -> typeParameterName + hasAbbreviatedType() && !resolveAliases -> abbreviatedType.typeAliasName + else -> className + } + + var typeName: TypeName = ClassName.bestGuess(nameResolver.getString(realType) + .replace("/", ".")) + + if (argumentList.isNotEmpty()) { + val remappedArgs: Array = argumentList.map { + val projection = if (it.hasProjection()) { + it.projection + } else null + if (it.hasType()) { + it.type.asTypeName(nameResolver, getTypeParameter, resolveAliases) + .let { typeName -> + projection?.let { + when (it) { + Projection.IN -> WildcardTypeName.supertypeOf(typeName) + Projection.OUT -> { + if (typeName == ANY) { + // This becomes a *, which we actually don't want here. + // List works with List<*>, but List<*> doesn't work with List + typeName + } else { + WildcardTypeName.subtypeOf(typeName) + } + } + Projection.STAR -> WildcardTypeName.subtypeOf(ANY) + Projection.INV -> TODO("INV projection is unsupported") + } + } ?: typeName + } + } else { + WildcardTypeName.subtypeOf(ANY) + } + }.toTypedArray() + typeName = ParameterizedTypeName.get(typeName as ClassName, *remappedArgs) + } + + if (nullable) { + typeName = typeName.asNullable() + } + + return typeName +} diff --git a/kotlin-codegen/integration-test/pom.xml b/kotlin-codegen/integration-test/pom.xml new file mode 100644 index 0000000..2ecc4b2 --- /dev/null +++ b/kotlin-codegen/integration-test/pom.xml @@ -0,0 +1,153 @@ + + + + 4.0.0 + + + com.squareup.moshi + moshi-parent + 1.6.0-SNAPSHOT + ../../pom.xml + + + moshi-kotlin-codegen-integration + + + + com.squareup.moshi + moshi + ${project.version} + + + com.squareup.moshi + moshi-kotlin-codegen-runtime + ${project.version} + + + org.jetbrains.kotlin + kotlin-stdlib + + + org.jetbrains.kotlin + kotlin-reflect + + + junit + junit + test + + + org.assertj + assertj-core + test + + + + + + kotlin-maven-plugin + org.jetbrains.kotlin + ${kotlin.version} + + + kapt + + kapt + + + + src/main/kotlin + src/main/java + + + + com.squareup.moshi + moshi-kotlin-codegen-compiler + ${project.version} + + + + + + compile + + compile + + + + src/main/kotlin + src/main/java + + + + + test-kapt + + test-kapt + + + + src/test/kotlin + src/test/java + + + + com.squareup.moshi + moshi-kotlin-codegen-compiler + ${project.version} + + + + + + test-compile + + test-compile + + + + src/test/kotlin + src/test/java + target/generated-sources/kapt/test + + + + + + + org.apache.maven.plugins + maven-compiler-plugin + 3.5.1 + + none + 1.6 + 1.6 + + + + + default-compile + none + + + + default-testCompile + none + + + java-compile + compile + + compile + + + + java-test-compile + test-compile + testCompile + + + + + + diff --git a/kotlin-codegen/integration-test/src/test/kotlin/com/squareup/moshi/DataClassesTest.kt b/kotlin-codegen/integration-test/src/test/kotlin/com/squareup/moshi/DataClassesTest.kt new file mode 100644 index 0000000..7dea3d8 --- /dev/null +++ b/kotlin-codegen/integration-test/src/test/kotlin/com/squareup/moshi/DataClassesTest.kt @@ -0,0 +1,280 @@ +/* + * Copyright (C) 2018 Square, Inc. + * + * 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 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.squareup.moshi + +import org.assertj.core.api.Assertions.assertThat +import org.assertj.core.api.Assertions.fail +import org.intellij.lang.annotations.Language +import org.junit.Test + +class DataClassesTest { + + private val moshi = Moshi.Builder().add(MoshiSerializableFactory()).build() + + @Test + fun jsonAnnotation() { + val adapter = moshi.adapter(JsonAnnotation::class.java) + + // Read + @Language("JSON") + val json = """{"foo": "bar"}""" + + val instance = adapter.fromJson(json)!! + assertThat(instance.bar).isEqualTo("bar") + + // Write + @Language("JSON") + val expectedJson = """{"foo":"baz"}""" + + assertThat(adapter.toJson(JsonAnnotation("baz"))).isEqualTo(expectedJson) + } + + @MoshiSerializable + data class JsonAnnotation(@Json(name = "foo") val bar: String) + + @Test + fun defaultValues() { + val adapter = moshi.adapter(DefaultValues::class.java) + + // Read/write with default values + @Language("JSON") + val json = """{"foo":"fooString"}""" + + val instance = adapter.fromJson(json)!! + assertThat(instance.foo).isEqualTo("fooString") + assertThat(instance.bar).isEqualTo("") + assertThat(instance.nullableBar).isNull() + assertThat(instance.bazList).apply { + isNotNull() + isEmpty() + } + + @Language("JSON") val expected = """{"foo":"fooString","bar":"","bazList":[]}""" + assertThat(adapter.toJson(DefaultValues("fooString"))).isEqualTo(expected) + + // Read/write with real values + @Language("JSON") + val json2 = """ + {"foo":"fooString","bar":"barString","nullableBar":"bar","bazList":["baz"]} + """.trimIndent() + + val instance2 = adapter.fromJson(json2)!! + assertThat(instance2.foo).isEqualTo("fooString") + assertThat(instance2.bar).isEqualTo("barString") + assertThat(instance2.nullableBar).isEqualTo("bar") + assertThat(instance2.bazList).containsExactly("baz") + assertThat(adapter.toJson(instance2)).isEqualTo(json2) + } + + @MoshiSerializable + data class DefaultValues(val foo: String, + val bar: String = "", + val nullableBar: String? = null, + val bazList: List = emptyList()) + + @Test + fun nullableArray() { + val adapter = moshi.adapter(NullableArray::class.java) + + @Language("JSON") + val json = """{"data":[null,"why"]}""" + + val instance = adapter.fromJson(json)!! + assertThat(instance.data).containsExactly(null, "why") + assertThat(adapter.toJson(instance)).isEqualTo(json) + } + + @MoshiSerializable + data class NullableArray(val data: Array) + + @Test + fun primitiveArray() { + val adapter = moshi.adapter(PrimitiveArray::class.java) + + @Language("JSON") + val json = """{"ints":[0,1]}""" + + val instance = adapter.fromJson(json)!! + assertThat(instance.ints).containsExactly(0, 1) + assertThat(adapter.toJson(instance)).isEqualTo(json) + } + + @MoshiSerializable + data class PrimitiveArray(val ints: IntArray) + + @Test + fun nullableTypes() { + val adapter = moshi.adapter(NullabeTypes::class.java) + + @Language("JSON") + val json = """{"foo":"foo","nullableString":null}""" + @Language("JSON") + val invalidJson = """{"foo":null,"nullableString":null}""" + + val instance = adapter.fromJson(json)!! + assertThat(instance.foo).isEqualTo("foo") + assertThat(instance.nullableString).isNull() + + try { + adapter.fromJson(invalidJson) + fail("The invalid json should have failed!") + } catch (e: JsonDataException) { + assertThat(e).hasMessageContaining("foo") + } + } + + @MoshiSerializable + data class NullabeTypes( + val foo: String, + val nullableString: String? + ) + + @Test + fun collections() { + val adapter = moshi.adapter(SpecialCollections::class.java) + + val specialCollections = SpecialCollections( + mutableListOf(), + mutableSetOf(), + mutableMapOf(), + emptyList(), + emptySet(), + emptyMap() + ) + + val json = adapter.toJson(specialCollections) + val newCollections = adapter.fromJson(json) + assertThat(newCollections).isEqualTo(specialCollections) + } + + @MoshiSerializable + data class SpecialCollections( + val mutableList: MutableList, + val mutableSet: MutableSet, + val mutableMap: MutableMap, + val immutableList: List, + val immutableSet: Set, + val immutableMap: Map + ) + + @Test + fun mutableProperties() { + val adapter = moshi.adapter(MutableProperties::class.java) + + val mutableProperties = MutableProperties( + "immutableProperty", + "mutableProperty", + mutableListOf("immutableMutableList"), + mutableListOf("immutableImmutableList"), + mutableListOf("mutableMutableList"), + mutableListOf("mutableImmutableList"), + "immutableProperty", + "mutableProperty", + mutableListOf("immutableMutableList"), + mutableListOf("immutableImmutableList"), + mutableListOf("mutableMutableList"), + mutableListOf("mutableImmutableList") + ) + + val json = adapter.toJson(mutableProperties) + val newMutableProperties = adapter.fromJson(json) + assertThat(newMutableProperties).isEqualTo(mutableProperties) + } + + @MoshiSerializable + data class MutableProperties( + val immutableProperty: String, + var mutableProperty: String, + val immutableMutableList: MutableList, + val immutableImmutableList: List, + var mutableMutableList: MutableList, + var mutableImmutableList: List, + val nullableImmutableProperty: String?, + var nullableMutableProperty: String?, + val nullableImmutableMutableList: MutableList?, + val nullableImmutableImmutableList: List?, + var nullableMutableMutableList: MutableList?, + var nullableMutableImmutableList: List + ) + + @Test + fun nullableTypeParams() { + val adapter = moshi.adapter>( + Types.newParameterizedType(NullableTypeParams::class.java, Int::class.javaObjectType)) + val nullSerializing = adapter.serializeNulls() + + val nullableTypeParams = NullableTypeParams( + listOf("foo", null, "bar"), + setOf("foo", null, "bar"), + mapOf("foo" to "bar", "baz" to null), + null + ) + + val noNullsTypeParams = NullableTypeParams( + nullableTypeParams.nullableList, + nullableTypeParams.nullableSet, + nullableTypeParams.nullableMap.filterValues { it != null }, + null + ) + + val json = adapter.toJson(nullableTypeParams) + val newNullableTypeParams = adapter.fromJson(json) + assertThat(newNullableTypeParams).isEqualTo(noNullsTypeParams) + + val nullSerializedJson = nullSerializing.toJson(nullableTypeParams) + val nullSerializedNullableTypeParams = adapter.fromJson(nullSerializedJson) + assertThat(nullSerializedNullableTypeParams).isEqualTo(nullableTypeParams) + } +} + +// Has to be outside to avoid Types seeing an owning class +@MoshiSerializable +data class NullableTypeParams( + val nullableList: List, + val nullableSet: Set, + val nullableMap: Map, + val nullableT: T? +) + +typealias TypeAliasName = String + +/** + * This is here mostly just to ensure it still compiles. Covers variance, @Json, default values, + * nullability, primitive arrays, and some wacky generics. + */ +@MoshiSerializable +data class SmokeTestType( + @Json(name = "first_name") val firstName: String, + @Json(name = "last_name") val lastName: String, + val age: Int, + val nationalities: List = emptyList(), + val weight: Float, + val tattoos: Boolean = false, + val race: String?, + val hasChildren: Boolean = false, + val favoriteFood: String? = null, + val favoriteDrink: String? = "Water", + val wildcardOut: List = emptyList(), + val wildcardIn: Array, + val any: List<*>, + val anyTwo: List, + val anyOut: List, + val favoriteThreeNumbers: IntArray, + val favoriteArrayValues: Array, + val favoriteNullableArrayValues: Array, + val nullableSetListMapArrayNullableIntWithDefault: Set>>>? = null, + val aliasedName: TypeAliasName = "Woah" +) diff --git a/kotlin-codegen/integration-test/src/test/kotlin/com/squareup/moshi/KotlinCodeGenTest.kt b/kotlin-codegen/integration-test/src/test/kotlin/com/squareup/moshi/KotlinCodeGenTest.kt new file mode 100644 index 0000000..6b12614 --- /dev/null +++ b/kotlin-codegen/integration-test/src/test/kotlin/com/squareup/moshi/KotlinCodeGenTest.kt @@ -0,0 +1,760 @@ +/* + * Copyright (C) 2017 Square, Inc. + * + * 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 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.squareup.moshi + +import org.assertj.core.api.Assertions.assertThat +import org.junit.Assert.fail +import org.junit.Ignore +import org.junit.Test +import java.io.ByteArrayOutputStream +import java.util.Locale +import java.util.SimpleTimeZone +import kotlin.annotation.AnnotationRetention.RUNTIME + +class KotlinCodeGenTest { + @Ignore @Test fun constructorParameters() { + val moshi = Moshi.Builder().add(MoshiSerializableFactory()).build() + val jsonAdapter = moshi.adapter(ConstructorParameters::class.java) + + val encoded = ConstructorParameters(3, 5) + assertThat(jsonAdapter.toJson(encoded)).isEqualTo("""{"a":3,"b":5}""") + + val decoded = jsonAdapter.fromJson("""{"a":4,"b":6}""")!! + assertThat(decoded.a).isEqualTo(4) + assertThat(decoded.b).isEqualTo(6) + } + + class ConstructorParameters(var a: Int, var b: Int) + + @Ignore @Test fun properties() { + + val moshi = Moshi.Builder().add(MoshiSerializableFactory()).build() + val jsonAdapter = moshi.adapter(Properties::class.java) + + val encoded = Properties() + encoded.a = 3 + encoded.b = 5 + assertThat(jsonAdapter.toJson(encoded)).isEqualTo("""{"a":3,"b":5}""") + + val decoded = jsonAdapter.fromJson("""{"a":3,"b":5}""")!! + assertThat(decoded.a).isEqualTo(3) + assertThat(decoded.b).isEqualTo(5) + } + + class Properties { + var a: Int = -1 + var b: Int = -1 + } + + @Ignore @Test fun constructorParametersAndProperties() { + val moshi = Moshi.Builder().add(MoshiSerializableFactory()).build() + val jsonAdapter = moshi.adapter(ConstructorParametersAndProperties::class.java) + + val encoded = ConstructorParametersAndProperties(3) + encoded.b = 5 + assertThat(jsonAdapter.toJson(encoded)).isEqualTo("""{"a":3,"b":5}""") + + val decoded = jsonAdapter.fromJson("""{"a":4,"b":6}""")!! + assertThat(decoded.a).isEqualTo(4) + assertThat(decoded.b).isEqualTo(6) + } + + class ConstructorParametersAndProperties(var a: Int) { + var b: Int = -1 + } + + @Ignore @Test fun immutableConstructorParameters() { + val moshi = Moshi.Builder().add(MoshiSerializableFactory()).build() + val jsonAdapter = moshi.adapter(ImmutableConstructorParameters::class.java) + + val encoded = ImmutableConstructorParameters(3, 5) + assertThat(jsonAdapter.toJson(encoded)).isEqualTo("""{"a":3,"b":5}""") + + val decoded = jsonAdapter.fromJson("""{"a":4,"b":6}""")!! + assertThat(decoded.a).isEqualTo(4) + assertThat(decoded.b).isEqualTo(6) + } + + class ImmutableConstructorParameters(val a: Int, val b: Int) + + @Ignore @Test fun immutableProperties() { + val moshi = Moshi.Builder().add(MoshiSerializableFactory()).build() + val jsonAdapter = moshi.adapter(ImmutableProperties::class.java) + + val encoded = ImmutableProperties(3, 5) + assertThat(jsonAdapter.toJson(encoded)).isEqualTo("""{"a":3,"b":5}""") + + val decoded = jsonAdapter.fromJson("""{"a":3,"b":5}""")!! + assertThat(decoded.a).isEqualTo(3) + assertThat(decoded.b).isEqualTo(5) + } + + class ImmutableProperties(a: Int, b: Int) { + val a = a + val b = b + } + + @Ignore @Test fun constructorDefaults() { + val moshi = Moshi.Builder().add(MoshiSerializableFactory()).build() + val jsonAdapter = moshi.adapter(ConstructorDefaultValues::class.java) + + val encoded = ConstructorDefaultValues(3, 5) + assertThat(jsonAdapter.toJson(encoded)).isEqualTo("""{"a":3,"b":5}""") + + val decoded = jsonAdapter.fromJson("""{"b":6}""")!! + assertThat(decoded.a).isEqualTo(-1) + assertThat(decoded.b).isEqualTo(6) + } + + class ConstructorDefaultValues(var a: Int = -1, var b: Int = -2) + + @Ignore @Test fun requiredValueAbsent() { + val moshi = Moshi.Builder().add(MoshiSerializableFactory()).build() + val jsonAdapter = moshi.adapter(RequiredValueAbsent::class.java) + + try { + jsonAdapter.fromJson("""{"a":4}""") + fail() + } catch(expected: JsonDataException) { + assertThat(expected).hasMessage("Required value b missing at $") + } + } + + class RequiredValueAbsent(var a: Int = 3, var b: Int) + + @Ignore @Test fun nonNullConstructorParameterCalledWithNullFailsWithJsonDataException() { + val moshi = Moshi.Builder().add(MoshiSerializableFactory()).build() + val jsonAdapter = moshi.adapter(HasNonNullConstructorParameter::class.java) + + try { + jsonAdapter.fromJson("{\"a\":null}") + fail() + } catch (expected: JsonDataException) { + assertThat(expected).hasMessage("Non-null value a was null at \$") + } + } + + class HasNonNullConstructorParameter(val a: String) + + @Ignore @Test fun nonNullPropertySetToNullFailsWithJsonDataException() { + val moshi = Moshi.Builder().add(MoshiSerializableFactory()).build() + val jsonAdapter = moshi.adapter(HasNonNullProperty::class.java) + + try { + jsonAdapter.fromJson("{\"a\":null}") + fail() + } catch (expected: JsonDataException) { + assertThat(expected).hasMessage("Non-null value a was null at \$") + } + } + + class HasNonNullProperty { + var a: String = "" + } + + @Ignore @Test fun duplicatedValue() { + val moshi = Moshi.Builder().add(MoshiSerializableFactory()).build() + val jsonAdapter = moshi.adapter(DuplicateValue::class.java) + + try { + jsonAdapter.fromJson("""{"a":4,"a":4}""") + fail() + } catch(expected: JsonDataException) { + assertThat(expected).hasMessage("Multiple values for a at $.a") + } + } + + class DuplicateValue(var a: Int = -1, var b: Int = -2) + + @Ignore @Test fun explicitNull() { + val moshi = Moshi.Builder().add(MoshiSerializableFactory()).build() + val jsonAdapter = moshi.adapter(ExplicitNull::class.java) + + val encoded = ExplicitNull(null, 5) + assertThat(jsonAdapter.toJson(encoded)).isEqualTo("""{"b":5}""") + assertThat(jsonAdapter.serializeNulls().toJson(encoded)).isEqualTo("""{"a":null,"b":5}""") + + val decoded = jsonAdapter.fromJson("""{"a":null,"b":6}""")!! + assertThat(decoded.a).isEqualTo(null) + assertThat(decoded.b).isEqualTo(6) + } + + class ExplicitNull(var a: Int?, var b: Int?) + + @Ignore @Test fun absentNull() { + val moshi = Moshi.Builder().add(MoshiSerializableFactory()).build() + val jsonAdapter = moshi.adapter(AbsentNull::class.java) + + val encoded = AbsentNull(null, 5) + assertThat(jsonAdapter.toJson(encoded)).isEqualTo("""{"b":5}""") + assertThat(jsonAdapter.serializeNulls().toJson(encoded)).isEqualTo("""{"a":null,"b":5}""") + + val decoded = jsonAdapter.fromJson("""{"b":6}""")!! + assertThat(decoded.a).isNull() + assertThat(decoded.b).isEqualTo(6) + } + + class AbsentNull(var a: Int?, var b: Int?) + + @Ignore @Test fun repeatedValue() { + val moshi = Moshi.Builder().add(MoshiSerializableFactory()).build() + val jsonAdapter = moshi.adapter(RepeatedValue::class.java) + + try { + jsonAdapter.fromJson("""{"a":4,"b":null,"b":6}""") + fail() + } catch(expected: JsonDataException) { + assertThat(expected).hasMessage("Multiple values for b at $.b") + } + } + + class RepeatedValue(var a: Int, var b: Int?) + + @Ignore @Test fun constructorParameterWithQualifier() { + val moshi = Moshi.Builder() + .add(MoshiSerializableFactory()) + .add(UppercaseJsonAdapter()) + .build() + val jsonAdapter = moshi.adapter(ConstructorParameterWithQualifier::class.java) + + val encoded = ConstructorParameterWithQualifier("Android", "Banana") + assertThat(jsonAdapter.toJson(encoded)).isEqualTo("""{"a":"ANDROID","b":"Banana"}""") + + val decoded = jsonAdapter.fromJson("""{"a":"Android","b":"Banana"}""")!! + assertThat(decoded.a).isEqualTo("android") + assertThat(decoded.b).isEqualTo("Banana") + } + + class ConstructorParameterWithQualifier(@Uppercase var a: String, var b: String) + + @Ignore @Test fun propertyWithQualifier() { + val moshi = Moshi.Builder() + .add(MoshiSerializableFactory()) + .add(UppercaseJsonAdapter()) + .build() + val jsonAdapter = moshi.adapter(PropertyWithQualifier::class.java) + + val encoded = PropertyWithQualifier() + encoded.a = "Android" + encoded.b = "Banana" + assertThat(jsonAdapter.toJson(encoded)).isEqualTo("""{"a":"ANDROID","b":"Banana"}""") + + val decoded = jsonAdapter.fromJson("""{"a":"Android","b":"Banana"}""")!! + assertThat(decoded.a).isEqualTo("android") + assertThat(decoded.b).isEqualTo("Banana") + } + + class PropertyWithQualifier { + @Uppercase var a: String = "" + var b: String = "" + } + + @Ignore @Test fun constructorParameterWithJsonName() { + val moshi = Moshi.Builder().add(MoshiSerializableFactory()).build() + val jsonAdapter = moshi.adapter(ConstructorParameterWithJsonName::class.java) + + val encoded = ConstructorParameterWithJsonName(3, 5) + assertThat(jsonAdapter.toJson(encoded)).isEqualTo("""{"key a":3,"b":5}""") + + val decoded = jsonAdapter.fromJson("""{"key a":4,"b":6}""")!! + assertThat(decoded.a).isEqualTo(4) + assertThat(decoded.b).isEqualTo(6) + } + + class ConstructorParameterWithJsonName(@Json(name = "key a") var a: Int, var b: Int) + + @Ignore @Test fun propertyWithJsonName() { + val moshi = Moshi.Builder().add(MoshiSerializableFactory()).build() + val jsonAdapter = moshi.adapter(PropertyWithJsonName::class.java) + + val encoded = PropertyWithJsonName() + encoded.a = 3 + encoded.b = 5 + assertThat(jsonAdapter.toJson(encoded)).isEqualTo("""{"key a":3,"b":5}""") + + val decoded = jsonAdapter.fromJson("""{"key a":4,"b":6}""")!! + assertThat(decoded.a).isEqualTo(4) + assertThat(decoded.b).isEqualTo(6) + } + + class PropertyWithJsonName { + @Json(name = "key a") var a: Int = -1 + var b: Int = -1 + } + + @Ignore @Test fun transientConstructorParameter() { + val moshi = Moshi.Builder().add(MoshiSerializableFactory()).build() + val jsonAdapter = moshi.adapter(TransientConstructorParameter::class.java) + + val encoded = TransientConstructorParameter(3, 5) + assertThat(jsonAdapter.toJson(encoded)).isEqualTo("""{"b":5}""") + + val decoded = jsonAdapter.fromJson("""{"a":4,"b":6}""")!! + assertThat(decoded.a).isEqualTo(-1) + assertThat(decoded.b).isEqualTo(6) + } + + class TransientConstructorParameter(@Transient var a: Int = -1, var b: Int = -1) + + @Ignore @Test fun requiredTransientConstructorParameterFails() { + val moshi = Moshi.Builder().add(MoshiSerializableFactory()).build() + try { + moshi.adapter(RequiredTransientConstructorParameter::class.java) + fail() + } catch (expected: IllegalArgumentException) { + assertThat(expected).hasMessage("No default value for transient constructor parameter #0 " + + "a of fun (kotlin.Int): " + + "com.squareup.moshi.KotlinJsonAdapterTest.RequiredTransientConstructorParameter") + } + } + + class RequiredTransientConstructorParameter(@Transient var a: Int) + + @Ignore @Test fun transientProperty() { + val moshi = Moshi.Builder().add(MoshiSerializableFactory()).build() + val jsonAdapter = moshi.adapter(TransientProperty::class.java) + + val encoded = TransientProperty() + encoded.a = 3 + encoded.b = 5 + assertThat(jsonAdapter.toJson(encoded)).isEqualTo("""{"b":5}""") + + val decoded = jsonAdapter.fromJson("""{"a":4,"b":6}""")!! + assertThat(decoded.a).isEqualTo(-1) + assertThat(decoded.b).isEqualTo(6) + } + + class TransientProperty { + @Transient var a: Int = -1 + var b: Int = -1 + } + + @Ignore @Test fun supertypeConstructorParameters() { + val moshi = Moshi.Builder().add(MoshiSerializableFactory()).build() + val jsonAdapter = moshi.adapter(SubtypeConstructorParameters::class.java) + + val encoded = SubtypeConstructorParameters(3, 5) + assertThat(jsonAdapter.toJson(encoded)).isEqualTo("""{"a":3,"b":5}""") + + val decoded = jsonAdapter.fromJson("""{"a":4,"b":6}""")!! + assertThat(decoded.a).isEqualTo(4) + assertThat(decoded.b).isEqualTo(6) + } + + open class SupertypeConstructorParameters(var a: Int) + + class SubtypeConstructorParameters(a: Int, var b: Int) : SupertypeConstructorParameters(a) + + @Ignore @Test fun supertypeProperties() { + val moshi = Moshi.Builder().add(MoshiSerializableFactory()).build() + val jsonAdapter = moshi.adapter(SubtypeProperties::class.java) + + val encoded = SubtypeProperties() + encoded.a = 3 + encoded.b = 5 + assertThat(jsonAdapter.toJson(encoded)).isEqualTo("""{"b":5,"a":3}""") + + val decoded = jsonAdapter.fromJson("""{"a":4,"b":6}""")!! + assertThat(decoded.a).isEqualTo(4) + assertThat(decoded.b).isEqualTo(6) + } + + open class SupertypeProperties { + var a: Int = -1 + } + + class SubtypeProperties : SupertypeProperties() { + var b: Int = -1 + } + + @Ignore @Test fun extendsPlatformClassWithPrivateField() { + val moshi = Moshi.Builder().add(MoshiSerializableFactory()).build() + val jsonAdapter = moshi.adapter(ExtendsPlatformClassWithPrivateField::class.java) + + val encoded = ExtendsPlatformClassWithPrivateField(3) + assertThat(jsonAdapter.toJson(encoded)).isEqualTo("""{"a":3}""") + + val decoded = jsonAdapter.fromJson("""{"a":4,"id":"B"}""")!! + assertThat(decoded.a).isEqualTo(4) + assertThat(decoded.id).isEqualTo("C") + } + + internal class ExtendsPlatformClassWithPrivateField(var a: Int) : SimpleTimeZone(0, "C") + + @Ignore @Test fun extendsPlatformClassWithProtectedField() { + val moshi = Moshi.Builder().add(MoshiSerializableFactory()).build() + val jsonAdapter = moshi.adapter(ExtendsPlatformClassWithProtectedField::class.java) + + val encoded = ExtendsPlatformClassWithProtectedField(3) + assertThat(jsonAdapter.toJson(encoded)).isEqualTo("""{"a":3,"buf":[0,0],"count":0}""") + + val decoded = jsonAdapter.fromJson("""{"a":4,"buf":[0,0],"size":0}""")!! + assertThat(decoded.a).isEqualTo(4) + assertThat(decoded.buf()).isEqualTo(ByteArray(2, { 0 })) + assertThat(decoded.count()).isEqualTo(0) + } + + internal class ExtendsPlatformClassWithProtectedField(var a: Int) : ByteArrayOutputStream(2) { + fun buf() = buf + fun count() = count + } + + @Ignore @Test fun platformTypeThrows() { + val moshi = Moshi.Builder().add(MoshiSerializableFactory()).build() + try { + moshi.adapter(Triple::class.java) + fail() + } catch (e: IllegalArgumentException) { + assertThat(e).hasMessage("Platform class kotlin.Triple annotated [] " + + "requires explicit JsonAdapter to be registered") + } + } + + @Ignore @Test fun privateConstructorParameters() { + val moshi = Moshi.Builder().add(MoshiSerializableFactory()).build() + val jsonAdapter = moshi.adapter(PrivateConstructorParameters::class.java) + + val encoded = PrivateConstructorParameters(3, 5) + assertThat(jsonAdapter.toJson(encoded)).isEqualTo("""{"a":3,"b":5}""") + + val decoded = jsonAdapter.fromJson("""{"a":4,"b":6}""")!! + assertThat(decoded.a()).isEqualTo(4) + assertThat(decoded.b()).isEqualTo(6) + } + + class PrivateConstructorParameters(private var a: Int, private var b: Int) { + fun a() = a + fun b() = b + } + + @Ignore @Test fun privateConstructor() { + val moshi = Moshi.Builder().add(MoshiSerializableFactory()).build() + val jsonAdapter = moshi.adapter(PrivateConstructor::class.java) + + val encoded = PrivateConstructor.newInstance(3, 5) + assertThat(jsonAdapter.toJson(encoded)).isEqualTo("""{"a":3,"b":5}""") + + val decoded = jsonAdapter.fromJson("""{"a":4,"b":6}""")!! + assertThat(decoded.a()).isEqualTo(4) + assertThat(decoded.b()).isEqualTo(6) + } + + class PrivateConstructor private constructor(var a: Int, var b: Int) { + fun a() = a + fun b() = b + companion object { + fun newInstance(a: Int, b: Int) = PrivateConstructor(a, b) + } + } + + @Ignore @Test fun privateProperties() { + val moshi = Moshi.Builder().add(MoshiSerializableFactory()).build() + val jsonAdapter = moshi.adapter(PrivateProperties::class.java) + + val encoded = PrivateProperties() + encoded.a(3) + encoded.b(5) + assertThat(jsonAdapter.toJson(encoded)).isEqualTo("""{"a":3,"b":5}""") + + val decoded = jsonAdapter.fromJson("""{"a":4,"b":6}""")!! + assertThat(decoded.a()).isEqualTo(4) + assertThat(decoded.b()).isEqualTo(6) + } + + class PrivateProperties { + var a: Int = -1 + var b: Int = -1 + + fun a() = a + + fun a(a: Int) { + this.a = a + } + + fun b() = b + + fun b(b: Int) { + this.b = b + } + } + + @Ignore @Test fun unsettablePropertyIgnored() { + val moshi = Moshi.Builder().add(MoshiSerializableFactory()).build() + val jsonAdapter = moshi.adapter(UnsettableProperty::class.java) + + val encoded = UnsettableProperty() + encoded.b = 5 + assertThat(jsonAdapter.toJson(encoded)).isEqualTo("""{"b":5}""") + + val decoded = jsonAdapter.fromJson("""{"a":4,"b":6}""")!! + assertThat(decoded.a).isEqualTo(-1) + assertThat(decoded.b).isEqualTo(6) + } + + class UnsettableProperty { + val a: Int = -1 + var b: Int = -1 + } + + @Ignore @Test fun getterOnlyNoBackingField() { + val moshi = Moshi.Builder().add(MoshiSerializableFactory()).build() + val jsonAdapter = moshi.adapter(GetterOnly::class.java) + + val encoded = GetterOnly(3, 5) + assertThat(jsonAdapter.toJson(encoded)).isEqualTo("""{"a":3,"b":5}""") + + val decoded = jsonAdapter.fromJson("""{"a":4,"b":6}""")!! + assertThat(decoded.a).isEqualTo(4) + assertThat(decoded.b).isEqualTo(6) + assertThat(decoded.total).isEqualTo(10) + } + + class GetterOnly(var a: Int, var b: Int) { + val total : Int + get() = a + b + } + + @Ignore @Test fun getterAndSetterNoBackingField() { + val moshi = Moshi.Builder().add(MoshiSerializableFactory()).build() + val jsonAdapter = moshi.adapter(GetterAndSetter::class.java) + + val encoded = GetterAndSetter(3, 5) + assertThat(jsonAdapter.toJson(encoded)).isEqualTo("""{"a":3,"b":5,"total":8}""") + + // Whether b is 6 or 7 is an implementation detail. Currently we call constructors then setters. + val decoded1 = jsonAdapter.fromJson("""{"a":4,"b":6,"total":11}""")!! + assertThat(decoded1.a).isEqualTo(4) + assertThat(decoded1.b).isEqualTo(7) + assertThat(decoded1.total).isEqualTo(11) + + // Whether b is 6 or 7 is an implementation detail. Currently we call constructors then setters. + val decoded2 = jsonAdapter.fromJson("""{"a":4,"total":11,"b":6}""")!! + assertThat(decoded2.a).isEqualTo(4) + assertThat(decoded2.b).isEqualTo(7) + assertThat(decoded2.total).isEqualTo(11) + } + + class GetterAndSetter(var a: Int, var b: Int) { + var total : Int + get() = a + b + set(value) { + b = value - a + } + } + + @Ignore @Test fun nonPropertyConstructorParameter() { + val moshi = Moshi.Builder().add(MoshiSerializableFactory()).build() + try { + moshi.adapter(NonPropertyConstructorParameter::class.java) + fail() + } catch(expected: IllegalArgumentException) { + assertThat(expected).hasMessage( + "No property for required constructor parameter #0 a of fun (" + + "kotlin.Int, kotlin.Int): ${NonPropertyConstructorParameter::class.qualifiedName}") + } + } + + class NonPropertyConstructorParameter(a: Int, val b: Int) + + @Ignore @Test fun kotlinEnumsAreNotCovered() { + val moshi = Moshi.Builder().add(MoshiSerializableFactory()).build() + val adapter = moshi.adapter(UsingEnum::class.java) + + assertThat(adapter.fromJson("""{"e": "A"}""")).isEqualTo(UsingEnum(KotlinEnum.A)) + } + + data class UsingEnum(val e: KotlinEnum) + + enum class KotlinEnum { + A, B + } + + @Ignore @Test fun interfacesNotSupported() { + val moshi = Moshi.Builder().add(MoshiSerializableFactory()).build() + try { + moshi.adapter(Interface::class.java) + fail() + } catch (e: IllegalArgumentException) { + assertThat(e).hasMessage("No JsonAdapter for interface " + + "com.squareup.moshi.KotlinJsonAdapterTest\$Interface annotated []") + } + } + + interface Interface + + @Ignore @Test fun abstractClassesNotSupported() { + val moshi = Moshi.Builder().add(MoshiSerializableFactory()).build() + try { + moshi.adapter(AbstractClass::class.java) + fail() + } catch (e: IllegalArgumentException) { + assertThat(e).hasMessage( + "Cannot serialize abstract class com.squareup.moshi.KotlinJsonAdapterTest\$AbstractClass") + } + } + + abstract class AbstractClass(val a: Int) + + @Ignore @Test fun innerClassesNotSupported() { + val moshi = Moshi.Builder().add(MoshiSerializableFactory()).build() + try { + moshi.adapter(InnerClass::class.java) + fail() + } catch (e: IllegalArgumentException) { + assertThat(e).hasMessage( + "Cannot serialize non-static nested class com.squareup.moshi.KotlinCodeGenTest\$InnerClass") + } + } + + inner class InnerClass(val a: Int) + + @Ignore @Test fun localClassesNotSupported() { + class LocalClass(val a: Int) + val moshi = Moshi.Builder().add(MoshiSerializableFactory()).build() + try { + moshi.adapter(LocalClass::class.java) + fail() + } catch (e: IllegalArgumentException) { + assertThat(e).hasMessage("Cannot serialize local class or object expression " + + "com.squareup.moshi.KotlinJsonAdapterTest\$localClassesNotSupported\$LocalClass") + } + } + + @Ignore @Test fun objectDeclarationsNotSupported() { + val moshi = Moshi.Builder().add(MoshiSerializableFactory()).build() + try { + moshi.adapter(ObjectDeclaration.javaClass) + fail() + } catch (e: IllegalArgumentException) { + assertThat(e).hasMessage("Cannot serialize object declaration " + + "com.squareup.moshi.KotlinJsonAdapterTest\$ObjectDeclaration") + } + } + + object ObjectDeclaration { + var a = 5 + } + + @Ignore @Test fun objectExpressionsNotSupported() { + val expression = object : Any() { + var a = 5 + } + val moshi = Moshi.Builder().add(MoshiSerializableFactory()).build() + try { + moshi.adapter(expression.javaClass) + fail() + } catch (e: IllegalArgumentException) { + assertThat(e).hasMessage("Cannot serialize local class or object expression " + + "com.squareup.moshi.KotlinJsonAdapterTest\$objectExpressionsNotSupported\$expression$1") + } + } + + @Ignore @Test fun manyProperties32() { + val moshi = Moshi.Builder().add(MoshiSerializableFactory()).build() + val jsonAdapter = moshi.adapter(ManyProperties32::class.java) + + val encoded = ManyProperties32( + 101, 102, 103, 104, 105, + 106, 107, 108, 109, 110, + 111, 112, 113, 114, 115, + 116, 117, 118, 119, 120, + 121, 122, 123, 124, 125, + 126, 127, 128, 129, 130, + 131, 132) + val json = (""" + |{ + |"v01":101,"v02":102,"v03":103,"v04":104,"v05":105, + |"v06":106,"v07":107,"v08":108,"v09":109,"v10":110, + |"v11":111,"v12":112,"v13":113,"v14":114,"v15":115, + |"v16":116,"v17":117,"v18":118,"v19":119,"v20":120, + |"v21":121,"v22":122,"v23":123,"v24":124,"v25":125, + |"v26":126,"v27":127,"v28":128,"v29":129,"v30":130, + |"v31":131,"v32":132 + |} + |""").trimMargin().replace("\n", "") + + assertThat(jsonAdapter.toJson(encoded)).isEqualTo(json) + + val decoded = jsonAdapter.fromJson(json)!! + assertThat(decoded.v01).isEqualTo(101) + assertThat(decoded.v32).isEqualTo(132) + } + + class ManyProperties32( + var v01: Int, var v02: Int, var v03: Int, var v04: Int, var v05: Int, + var v06: Int, var v07: Int, var v08: Int, var v09: Int, var v10: Int, + var v11: Int, var v12: Int, var v13: Int, var v14: Int, var v15: Int, + var v16: Int, var v17: Int, var v18: Int, var v19: Int, var v20: Int, + var v21: Int, var v22: Int, var v23: Int, var v24: Int, var v25: Int, + var v26: Int, var v27: Int, var v28: Int, var v29: Int, var v30: Int, + var v31: Int, var v32: Int) + + @Ignore @Test fun manyProperties33() { + val moshi = Moshi.Builder().add(MoshiSerializableFactory()).build() + val jsonAdapter = moshi.adapter(ManyProperties33::class.java) + + val encoded = ManyProperties33( + 101, 102, 103, 104, 105, + 106, 107, 108, 109, 110, + 111, 112, 113, 114, 115, + 116, 117, 118, 119, 120, + 121, 122, 123, 124, 125, + 126, 127, 128, 129, 130, + 131, 132, 133) + val json = (""" + |{ + |"v01":101,"v02":102,"v03":103,"v04":104,"v05":105, + |"v06":106,"v07":107,"v08":108,"v09":109,"v10":110, + |"v11":111,"v12":112,"v13":113,"v14":114,"v15":115, + |"v16":116,"v17":117,"v18":118,"v19":119,"v20":120, + |"v21":121,"v22":122,"v23":123,"v24":124,"v25":125, + |"v26":126,"v27":127,"v28":128,"v29":129,"v30":130, + |"v31":131,"v32":132,"v33":133 + |} + |""").trimMargin().replace("\n", "") + + assertThat(jsonAdapter.toJson(encoded)).isEqualTo(json) + + val decoded = jsonAdapter.fromJson(json)!! + assertThat(decoded.v01).isEqualTo(101) + assertThat(decoded.v32).isEqualTo(132) + assertThat(decoded.v33).isEqualTo(133) + } + + class ManyProperties33( + var v01: Int, var v02: Int, var v03: Int, var v04: Int, var v05: Int, + var v06: Int, var v07: Int, var v08: Int, var v09: Int, var v10: Int, + var v11: Int, var v12: Int, var v13: Int, var v14: Int, var v15: Int, + var v16: Int, var v17: Int, var v18: Int, var v19: Int, var v20: Int, + var v21: Int, var v22: Int, var v23: Int, var v24: Int, var v25: Int, + var v26: Int, var v27: Int, var v28: Int, var v29: Int, var v30: Int, + var v31: Int, var v32: Int, var v33: Int) + + // TODO(jwilson): resolve generic types? + + @Retention(RUNTIME) + @JsonQualifier + annotation class Uppercase + + class UppercaseJsonAdapter { + @ToJson fun toJson(@Uppercase s: String) : String { + return s.toUpperCase(Locale.US) + } + @FromJson @Uppercase fun fromJson(s: String) : String { + return s.toLowerCase(Locale.US) + } + } +} diff --git a/kotlin-codegen/runtime/pom.xml b/kotlin-codegen/runtime/pom.xml new file mode 100644 index 0000000..08b8e36 --- /dev/null +++ b/kotlin-codegen/runtime/pom.xml @@ -0,0 +1,82 @@ + + + + 4.0.0 + + + com.squareup.moshi + moshi-parent + 1.6.0-SNAPSHOT + ../../pom.xml + + + moshi-kotlin-codegen-runtime + + + + com.squareup.moshi + moshi + ${project.version} + + + junit + junit + test + + + org.assertj + assertj-core + test + + + org.jetbrains.kotlin + kotlin-stdlib + + + + + + + org.jetbrains.kotlin + kotlin-maven-plugin + ${kotlin.version} + + + compile + compile + + compile + + + + test-compile + test-compile + + test-compile + + + + + + org.apache.maven.plugins + maven-compiler-plugin + + + compile + compile + + compile + + + + testCompile + test-compile + + testCompile + + + + + + + diff --git a/kotlin-codegen/runtime/src/main/java/com/squareup/moshi/MoshiSerializable.kt b/kotlin-codegen/runtime/src/main/java/com/squareup/moshi/MoshiSerializable.kt new file mode 100644 index 0000000..d685765 --- /dev/null +++ b/kotlin-codegen/runtime/src/main/java/com/squareup/moshi/MoshiSerializable.kt @@ -0,0 +1,77 @@ +/* + * Copyright (C) 2018 Square, Inc. + * + * 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 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.squareup.moshi + +import java.lang.reflect.InvocationTargetException +import java.lang.reflect.ParameterizedType +import java.lang.reflect.Type +import kotlin.annotation.AnnotationRetention.RUNTIME +import kotlin.annotation.AnnotationTarget.CLASS + +@Retention(RUNTIME) +@Target(CLASS) +annotation class MoshiSerializable + +class MoshiSerializableFactory : JsonAdapter.Factory { + + override fun create(type: Type, annotations: Set, moshi: Moshi): JsonAdapter<*>? { + + val rawType = Types.getRawType(type) + if (!rawType.isAnnotationPresent(MoshiSerializable::class.java)) { + return null + } + + val clsName = rawType.name.replace("$", "_") + val constructor = try { + @Suppress("UNCHECKED_CAST") + val bindingClass = rawType.classLoader + .loadClass(clsName + "JsonAdapter") as Class> + if (type is ParameterizedType) { + // This is generic, use the two param moshi + type constructor + bindingClass.getDeclaredConstructor(Moshi::class.java, Array::class.java) + } else { + // The standard single param moshi constructor + bindingClass.getDeclaredConstructor(Moshi::class.java) + } + } catch (e: ClassNotFoundException) { + throw RuntimeException("Unable to find generated Moshi adapter class for $clsName", e) + } catch (e: NoSuchMethodException) { + throw RuntimeException("Unable to find generated Moshi adapter constructor for $clsName", e) + } + + try { + return when { + constructor.parameterTypes.size == 1 -> constructor.newInstance(moshi) + type is ParameterizedType -> constructor.newInstance(moshi, type.actualTypeArguments) + else -> throw IllegalStateException("Unable to handle type $type") + } + } catch (e: IllegalAccessException) { + throw RuntimeException("Unable to invoke $constructor", e) + } catch (e: InstantiationException) { + throw RuntimeException("Unable to invoke $constructor", e) + } catch (e: InvocationTargetException) { + val cause = e.cause + if (cause is RuntimeException) { + throw cause + } + if (cause is Error) { + throw cause + } + throw RuntimeException( + "Could not create generated JsonAdapter instance for type $rawType", cause) + } + } +} diff --git a/kotlin/pom.xml b/kotlin/pom.xml index 7c45388..0c94d65 100644 --- a/kotlin/pom.xml +++ b/kotlin/pom.xml @@ -47,6 +47,7 @@ org.jetbrains.kotlin kotlin-maven-plugin + ${kotlin.version} compile diff --git a/moshi/src/main/java/com/squareup/moshi/Moshi.java b/moshi/src/main/java/com/squareup/moshi/Moshi.java index 509d18b..41708c6 100644 --- a/moshi/src/main/java/com/squareup/moshi/Moshi.java +++ b/moshi/src/main/java/com/squareup/moshi/Moshi.java @@ -23,6 +23,7 @@ import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; import java.util.LinkedHashMap; +import java.util.LinkedHashSet; import java.util.List; import java.util.Map; import java.util.Set; @@ -73,6 +74,18 @@ public final class Moshi { Collections.singleton(Types.createJsonQualifierImplementation(annotationType))); } + @CheckReturnValue + public JsonAdapter adapter(Type type, Class... annotationTypes) { + if (annotationTypes.length == 1) { + return adapter(type, annotationTypes[0]); + } + Set annotations = new LinkedHashSet<>(annotationTypes.length); + for (Class annotationType : annotationTypes) { + annotations.add(Types.createJsonQualifierImplementation(annotationType)); + } + return adapter(type, Collections.unmodifiableSet(annotations)); + } + @CheckReturnValue @SuppressWarnings("unchecked") // Factories are required to return only matching JsonAdapters. public JsonAdapter adapter(Type type, Set annotations) { diff --git a/pom.xml b/pom.xml index b668d3a..30b6416 100644 --- a/pom.xml +++ b/pom.xml @@ -22,12 +22,15 @@ examples adapters kotlin + kotlin-codegen/compiler + kotlin-codegen/integration-test + kotlin-codegen/runtime UTF-8 1.7 - 1.1.60 + 1.2.21 1.13.0