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 extends Annotation>... annotationTypes) {
+ if (annotationTypes.length == 1) {
+ return adapter(type, annotationTypes[0]);
+ }
+ Set annotations = new LinkedHashSet<>(annotationTypes.length);
+ for (Class extends Annotation> 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 extends Annotation> 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