Move the Kotlin code generator to the kotlin/ module

This commit is contained in:
Jesse Wilson
2018-05-14 12:25:25 -04:00
parent c5c4cac6c3
commit 9f69029ef0
22 changed files with 75 additions and 58 deletions

156
kotlin/codegen/pom.xml Normal file
View File

@@ -0,0 +1,156 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>com.squareup.moshi</groupId>
<artifactId>moshi-parent</artifactId>
<version>1.6.0-SNAPSHOT</version>
<relativePath>../../pom.xml</relativePath>
</parent>
<artifactId>moshi-kotlin-codegen</artifactId>
<dependencies>
<dependency>
<groupId>com.squareup.moshi</groupId>
<artifactId>moshi</artifactId>
<version>${project.version}</version>
</dependency>
<dependency>
<groupId>org.jetbrains.kotlin</groupId>
<artifactId>kotlin-stdlib</artifactId>
</dependency>
<dependency>
<groupId>com.google.auto</groupId>
<artifactId>auto-common</artifactId>
<version>0.10</version>
</dependency>
<dependency>
<groupId>com.google.auto.service</groupId>
<artifactId>auto-service</artifactId>
<version>1.0-rc4</version>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>com.squareup</groupId>
<artifactId>kotlinpoet</artifactId>
<version>0.7.0</version>
</dependency>
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.assertj</groupId>
<artifactId>assertj-core</artifactId>
<scope>test</scope>
</dependency>
<!--
The Kotlin compiler must be near the end of the list because its .jar file includes an
obsolete version of Guava!
-->
<dependency>
<groupId>org.jetbrains.kotlin</groupId>
<artifactId>kotlin-compiler-embeddable</artifactId>
</dependency>
<dependency>
<groupId>org.jetbrains.kotlin</groupId>
<artifactId>kotlin-annotation-processing-embeddable</artifactId>
</dependency>
<dependency>
<groupId>me.eugeniomarletti.kotlin.metadata</groupId>
<artifactId>kotlin-metadata</artifactId>
<version>1.4.0</version>
</dependency>
<!--
Though we don't use compile-testing, including it is a convenient way to get tools.jar on the
classpath. This dependency is required by kapt3.
-->
<dependency>
<groupId>com.google.testing.compile</groupId>
<artifactId>compile-testing</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.jetbrains.kotlin</groupId>
<artifactId>kotlin-maven-plugin</artifactId>
<version>${kotlin.version}</version>
<executions>
<execution>
<id>kapt</id>
<goals>
<goal>kapt</goal>
</goals>
<configuration>
<sourceDirs>
<sourceDir>src/main/kotlin</sourceDir>
<sourceDir>src/main/java</sourceDir>
</sourceDirs>
<annotationProcessorPaths>
<annotationProcessorPath>
<groupId>com.google.auto.service</groupId>
<artifactId>auto-service</artifactId>
<version>1.0-rc4</version>
</annotationProcessorPath>
</annotationProcessorPaths>
</configuration>
</execution>
<execution>
<id>compile</id>
<phase>compile</phase>
<goals>
<goal>compile</goal>
</goals>
</execution>
<execution>
<id>test-compile</id>
<phase>test-compile</phase>
<goals>
<goal>test-compile</goal>
</goals>
</execution>
</executions>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<executions>
<execution>
<id>compile</id>
<phase>compile</phase>
<goals>
<goal>compile</goal>
</goals>
</execution>
<execution>
<id>testCompile</id>
<phase>test-compile</phase>
<goals>
<goal>testCompile</goal>
</goals>
</execution>
</executions>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-surefire-plugin</artifactId>
<version>2.21.0</version>
<configuration>
<!--
Suppress the surefire classloader which prevents introspecting the classpath.
http://maven.apache.org/surefire/maven-surefire-plugin/examples/class-loading.html
-->
<useManifestOnlyJar>false</useManifestOnlyJar>
</configuration>
</plugin>
</plugins>
</build>
</project>

View File

@@ -0,0 +1,341 @@
/*
* 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.kotlin.codegen
import com.squareup.kotlinpoet.ARRAY
import com.squareup.kotlinpoet.AnnotationSpec
import com.squareup.kotlinpoet.ClassName
import com.squareup.kotlinpoet.CodeBlock
import com.squareup.kotlinpoet.FileSpec
import com.squareup.kotlinpoet.FunSpec
import com.squareup.kotlinpoet.KModifier
import com.squareup.kotlinpoet.NameAllocator
import com.squareup.kotlinpoet.ParameterSpec
import com.squareup.kotlinpoet.ParameterizedTypeName
import com.squareup.kotlinpoet.PropertySpec
import com.squareup.kotlinpoet.TypeSpec
import com.squareup.kotlinpoet.TypeVariableName
import com.squareup.kotlinpoet.asClassName
import com.squareup.kotlinpoet.asTypeName
import com.squareup.moshi.JsonAdapter
import com.squareup.moshi.JsonDataException
import com.squareup.moshi.JsonReader
import com.squareup.moshi.JsonWriter
import com.squareup.moshi.Moshi
import me.eugeniomarletti.kotlin.metadata.isDataClass
import me.eugeniomarletti.kotlin.metadata.shadow.metadata.ProtoBuf.Visibility
import me.eugeniomarletti.kotlin.metadata.visibility
import java.lang.reflect.Type
import javax.annotation.processing.Messager
import javax.lang.model.element.TypeElement
/** Generates a JSON adapter for a target type. */
internal class AdapterGenerator(
target: TargetType,
private val propertyList: List<PropertyGenerator>
) {
private val className = target.name
private val isDataClass = target.proto.isDataClass
private val hasCompanionObject = target.hasCompanionObject
private val visibility = target.proto.visibility!!
private val typeVariables = target.typeVariables
private val nameAllocator = NameAllocator()
private val adapterName = "${className.simpleNames().joinToString(separator = "_")}JsonAdapter"
private val originalTypeName = target.element.asType().asTypeName()
private val moshiParam = ParameterSpec.builder(
nameAllocator.newName("moshi"),
Moshi::class).build()
private val typesParam = ParameterSpec.builder(
nameAllocator.newName("types"),
ParameterizedTypeName.get(ARRAY, Type::class.asTypeName()))
.build()
private val readerParam = ParameterSpec.builder(
nameAllocator.newName("reader"),
JsonReader::class)
.build()
private val writerParam = ParameterSpec.builder(
nameAllocator.newName("writer"),
JsonWriter::class)
.build()
private val valueParam = ParameterSpec.builder(
nameAllocator.newName("value"),
originalTypeName.asNullable())
.build()
private val jsonAdapterTypeName = ParameterizedTypeName.get(
JsonAdapter::class.asClassName(), originalTypeName)
// selectName() API setup
private val optionsProperty = PropertySpec.builder(
nameAllocator.newName("options"), JsonReader.Options::class.asTypeName(),
KModifier.PRIVATE)
.initializer("%T.of(${propertyList.map { it.jsonName }
.joinToString(", ") { "\"$it\"" }})", JsonReader.Options::class.asTypeName())
.build()
fun generateFile(messager: Messager, generatedOption: TypeElement?): FileSpec {
for (property in propertyList) {
property.allocateNames(nameAllocator)
}
val result = FileSpec.builder(className.packageName(), adapterName)
result.addComment("Code generated by moshi-kotlin-codegen. Do not edit.")
if (hasCompanionObject) {
result.addFunction(generateJsonAdapterFun())
}
result.addType(generateType(messager, generatedOption))
return result.build()
}
private fun generateType(messager: Messager, generatedOption: TypeElement?): TypeSpec {
val result = TypeSpec.classBuilder(adapterName)
generatedOption?.let {
result.addAnnotation(AnnotationSpec.builder(it.asClassName())
.addMember("%S", JsonClassCodegenProcessor::class.java.canonicalName)
.addMember("%S", "https://github.com/square/moshi")
.build())
}
result.superclass(jsonAdapterTypeName)
if (typeVariables.isNotEmpty()) {
result.addTypeVariables(typeVariables)
}
// TODO make this configurable. Right now it just matches the source model
if (visibility == Visibility.INTERNAL) {
result.addModifiers(KModifier.INTERNAL)
}
result.primaryConstructor(generateConstructor())
val typeRenderer: TypeRenderer = object : TypeRenderer() {
override fun renderTypeVariable(typeVariable: TypeVariableName): CodeBlock {
val index = typeVariables.indexOfFirst { it == typeVariable }
check(index != -1) { "Unexpected type variable $typeVariable" }
return CodeBlock.of("%N[%L]", typesParam, index)
}
}
result.addProperty(optionsProperty)
for (uniqueAdapter in propertyList.distinctBy { it.delegateKey }) {
result.addProperty(uniqueAdapter.delegateKey.generateProperty(
nameAllocator, typeRenderer, moshiParam, messager))
}
result.addFunction(generateToStringFun())
result.addFunction(generateFromJsonFun())
result.addFunction(generateToJsonFun())
return result.build()
}
private fun generateConstructor(): FunSpec {
val result = FunSpec.constructorBuilder()
result.addParameter(moshiParam)
if (typeVariables.isNotEmpty()) {
result.addParameter(typesParam)
}
return result.build()
}
private fun generateToStringFun(): FunSpec {
return FunSpec.builder("toString")
.addModifiers(KModifier.OVERRIDE)
.returns(String::class)
.addStatement("return %S",
"GeneratedJsonAdapter(${originalTypeName.rawType().simpleNames().joinToString(".")})")
.build()
}
private fun generateFromJsonFun(): FunSpec {
val resultName = nameAllocator.newName("result")
val result = FunSpec.builder("fromJson")
.addModifiers(KModifier.OVERRIDE)
.addParameter(readerParam)
.returns(originalTypeName)
for (property in propertyList) {
result.addCode("%L", property.generateLocalProperty())
if (property.differentiateAbsentFromNull) {
result.addCode("%L", property.generateLocalIsPresentProperty())
}
}
result.addStatement("%N.beginObject()", readerParam)
result.beginControlFlow("while (%N.hasNext())", readerParam)
result.beginControlFlow("when (%N.selectName(%N))", readerParam, optionsProperty)
propertyList.forEachIndexed { index, property ->
if (property.differentiateAbsentFromNull) {
result.beginControlFlow("%L -> ", index)
if (property.delegateKey.nullable) {
result.addStatement("%N = %N.fromJson(%N)",
property.localName, nameAllocator.get(property.delegateKey), readerParam)
} else {
result.addStatement("%N = %N.fromJson(%N)" +
" ?: throw %T(\"Non-null value '%N' was null at \${%N.path}\")",
property.localName, nameAllocator.get(property.delegateKey), readerParam,
JsonDataException::class, property.localName, readerParam)
}
result.addStatement("%N = true", property.localIsPresentName)
result.endControlFlow()
} else {
if (property.delegateKey.nullable) {
result.addStatement("%L -> %N = %N.fromJson(%N)",
index, property.localName, nameAllocator.get(property.delegateKey), readerParam)
} else {
result.addStatement("%L -> %N = %N.fromJson(%N)" +
" ?: throw %T(\"Non-null value '%N' was null at \${%N.path}\")",
index, property.localName, nameAllocator.get(property.delegateKey), readerParam,
JsonDataException::class, property.localName, readerParam)
}
}
}
result.beginControlFlow("-1 ->")
result.addComment("Unknown name, skip it.")
result.addStatement("%N.nextName()", readerParam)
result.addStatement("%N.skipValue()", readerParam)
result.endControlFlow()
result.endControlFlow() // when
result.endControlFlow() // while
result.addStatement("%N.endObject()", readerParam)
// Call the constructor providing only required parameters.
var hasOptionalParameters = false
result.addCode("%[var %N = %T(", resultName, originalTypeName)
var separator = "\n"
for (property in propertyList) {
if (!property.hasConstructorParameter) {
continue
}
if (property.hasDefault) {
hasOptionalParameters = true
continue
}
result.addCode(separator)
result.addCode("%N = %N", property.name, property.localName)
if (property.isRequired) {
result.addCode(" ?: throw %T(\"Required property '%L' missing at \${%N.path}\")",
JsonDataException::class, property.localName, readerParam)
}
separator = ",\n"
}
result.addCode(")%]\n", originalTypeName)
// Call either the constructor again, or the copy() method, this time providing any optional
// parameters that we have.
if (hasOptionalParameters) {
if (isDataClass) {
result.addCode("%[%1N = %1N.copy(", resultName)
} else {
result.addCode("%[%1N = %2T(", resultName, originalTypeName)
}
separator = "\n"
for (property in propertyList) {
if (!property.hasConstructorParameter) {
continue // No constructor parameter for this property.
}
if (isDataClass && !property.hasDefault) {
continue // Property already assigned.
}
result.addCode(separator)
if (property.differentiateAbsentFromNull) {
result.addCode("%2N = if (%3N) %4N else %1N.%2N",
resultName, property.name, property.localIsPresentName, property.localName)
} else {
result.addCode("%2N = %3N ?: %1N.%2N", resultName, property.name, property.localName)
}
separator = ",\n"
}
result.addCode("%])\n")
}
// Assign properties not present in the constructor.
for (property in propertyList) {
if (property.hasConstructorParameter) {
continue // Property already handled.
}
if (property.differentiateAbsentFromNull) {
result.addStatement("%1N.%2N = if (%3N) %4N else %1N.%2N",
resultName, property.name, property.localIsPresentName, property.localName)
} else {
result.addStatement("%1N.%2N = %3N ?: %1N.%2N",
resultName, property.name, property.localName)
}
}
result.addStatement("return %1N", resultName)
return result.build()
}
private fun generateToJsonFun(): FunSpec {
val result = FunSpec.builder("toJson")
.addModifiers(KModifier.OVERRIDE)
.addParameter(writerParam)
.addParameter(valueParam)
result.beginControlFlow("if (%N == null)", valueParam)
result.addStatement("throw %T(%S)", NullPointerException::class,
"${valueParam.name} was null! Wrap in .nullSafe() to write nullable values.")
result.endControlFlow()
result.addStatement("%N.beginObject()", writerParam)
propertyList.forEach { property ->
result.addStatement("%N.name(%S)", writerParam, property.jsonName)
result.addStatement("%N.toJson(%N, %N.%L)",
nameAllocator.get(property.delegateKey), writerParam, valueParam, property.name)
}
result.addStatement("%N.endObject()", writerParam)
return result.build()
}
private fun generateJsonAdapterFun(): FunSpec {
val rawType = when (originalTypeName) {
is TypeVariableName -> throw IllegalArgumentException("Cannot get raw type of TypeVariable!")
is ParameterizedTypeName -> originalTypeName.rawType
else -> originalTypeName as ClassName
}
val result = FunSpec.builder("jsonAdapter")
.receiver(rawType.nestedClass("Companion"))
.returns(jsonAdapterTypeName)
.addParameter(moshiParam)
if (visibility == Visibility.INTERNAL) {
result.addModifiers(KModifier.INTERNAL)
}
if (typeVariables.isNotEmpty()) {
result.addParameter(typesParam)
result.addTypeVariables(typeVariables)
result.addStatement("return %N(%N, %N)", adapterName, moshiParam, typesParam)
} else {
result.addStatement("return %N(%N)", adapterName, moshiParam)
}
return result.build()
}
}

View File

@@ -0,0 +1,71 @@
/*
* 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.kotlin.codegen
import com.squareup.kotlinpoet.TypeName
import com.squareup.kotlinpoet.TypeVariableName
import com.squareup.kotlinpoet.asTypeName
import javax.lang.model.element.TypeElement
import javax.lang.model.type.DeclaredType
import javax.lang.model.util.Types
/**
* A concrete type like `List<String>` with enough information to know how to resolve its type
* variables.
*/
internal class AppliedType private constructor(
val element: TypeElement,
val resolver: TypeResolver,
private val mirror: DeclaredType
) {
/** Returns all supertypes of this, recursively. Includes both interface and class supertypes. */
fun supertypes(
types: Types,
result: MutableSet<AppliedType> = mutableSetOf()
): Set<AppliedType> {
result.add(this)
for (supertype in types.directSupertypes(mirror)) {
val supertypeDeclaredType = supertype as DeclaredType
val supertypeElement = supertypeDeclaredType.asElement() as TypeElement
val appliedSupertype = AppliedType(supertypeElement,
resolver(supertypeElement, supertypeDeclaredType), supertypeDeclaredType)
appliedSupertype.supertypes(types, result)
}
return result
}
/** Returns a resolver that uses `element` and `mirror` to resolve type parameters. */
private fun resolver(element: TypeElement, mirror: DeclaredType): TypeResolver {
return object : TypeResolver() {
override fun resolveTypeVariable(typeVariable: TypeVariableName): TypeName {
val index = element.typeParameters.indexOfFirst {
it.simpleName.toString() == typeVariable.name
}
check(index != -1) { "Unexpected type variable $typeVariable in $mirror" }
val argument = mirror.typeArguments[index]
return argument.asTypeName()
}
}
}
override fun toString() = mirror.toString()
companion object {
fun get(typeElement: TypeElement): AppliedType {
return AppliedType(typeElement, TypeResolver(), typeElement.asType() as DeclaredType)
}
}
}

View File

@@ -0,0 +1,129 @@
/*
* 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.kotlin.codegen
import com.google.auto.common.MoreTypes
import com.squareup.kotlinpoet.AnnotationSpec
import com.squareup.kotlinpoet.AnnotationSpec.UseSiteTarget.FIELD
import com.squareup.kotlinpoet.ClassName
import com.squareup.kotlinpoet.CodeBlock
import com.squareup.kotlinpoet.KModifier
import com.squareup.kotlinpoet.NameAllocator
import com.squareup.kotlinpoet.ParameterSpec
import com.squareup.kotlinpoet.ParameterizedTypeName
import com.squareup.kotlinpoet.PropertySpec
import com.squareup.kotlinpoet.TypeName
import com.squareup.kotlinpoet.TypeVariableName
import com.squareup.kotlinpoet.WildcardTypeName
import com.squareup.kotlinpoet.asTypeName
import com.squareup.moshi.JsonAdapter
import com.squareup.moshi.Types
import java.lang.annotation.ElementType
import java.lang.annotation.RetentionPolicy
import javax.annotation.processing.Messager
import javax.lang.model.element.AnnotationMirror
import javax.tools.Diagnostic.Kind.ERROR
/** A JsonAdapter that can be used to encode and decode a particular field. */
internal data class DelegateKey(
private val type: TypeName,
private val jsonQualifiers: Set<AnnotationMirror>
) {
val nullable get() = type.nullable
/** Returns an adapter to use when encoding and decoding this property. */
fun generateProperty(
nameAllocator: NameAllocator,
typeRenderer: TypeRenderer,
moshiParameter: ParameterSpec,
messager: Messager): PropertySpec {
fun AnnotationMirror.validate(): AnnotationMirror {
// Check java types since that covers both java and kotlin annotations
val annotationElement = MoreTypes.asTypeElement(annotationType)
annotationElement.getAnnotation(java.lang.annotation.Retention::class.java)?.let {
if (it.value != RetentionPolicy.RUNTIME) {
messager.printMessage(ERROR, "JsonQualifier " +
"@${MoreTypes.asTypeElement(annotationType).simpleName} must have RUNTIME retention")
}
}
annotationElement.getAnnotation(java.lang.annotation.Target::class.java)?.let {
if (ElementType.FIELD !in it.value) {
messager.printMessage(ERROR, "JsonQualifier " +
"@${MoreTypes.asTypeElement(annotationType).simpleName} must support FIELD target")
}
}
return this
}
jsonQualifiers.forEach { it.validate() }
val qualifierNames = jsonQualifiers.joinToString("") {
"At${it.annotationType.asElement().simpleName}"
}
val adapterName = nameAllocator.newName(
"${type.toVariableName().decapitalize()}${qualifierNames}Adapter", this)
val adapterTypeName = ParameterizedTypeName.get(
JsonAdapter::class.asTypeName(), type)
val qualifiers = jsonQualifiers
val standardArgs = arrayOf(moshiParameter,
if (type is ClassName && qualifiers.isEmpty()) {
""
} else {
CodeBlock.of("<%T>", type)
},
typeRenderer.render(type))
val standardArgsSize = standardArgs.size + 1
val (initializerString, args) = when {
qualifiers.isEmpty() -> "" to emptyArray()
else -> {
", %${standardArgsSize}T.getFieldJsonQualifierAnnotations(javaClass, " +
"%${standardArgsSize + 1}S)" to arrayOf(Types::class.asTypeName(), adapterName)
}
}
val finalArgs = arrayOf(*standardArgs, *args)
val nullModifier = if (nullable) ".nullSafe()" else ".nonNull()"
return PropertySpec.builder(adapterName, adapterTypeName, KModifier.PRIVATE)
.addAnnotations(qualifiers.map {
AnnotationSpec.get(it).toBuilder().useSiteTarget(FIELD).build()
})
.initializer("%1N.adapter%2L(%3L$initializerString)$nullModifier", *finalArgs)
.build()
}
}
/**
* Returns a suggested variable name derived from a list of type names. This just concatenates,
* yielding types like MapOfStringLong.
*/
private fun List<TypeName>.toVariableNames() = joinToString("") { it.toVariableName() }
/** Returns a suggested variable name derived from a type name, like nullableListOfString. */
private fun TypeName.toVariableName(): String {
val base = when (this) {
is ClassName -> simpleName()
is ParameterizedTypeName -> rawType.simpleName() + "Of" + typeArguments.toVariableNames()
is WildcardTypeName -> (lowerBounds + upperBounds).toVariableNames()
is TypeVariableName -> name + bounds.toVariableNames()
else -> throw IllegalArgumentException("Unrecognized type! $this")
}
return if (nullable) {
"Nullable$base"
} else {
base
}
}

View File

@@ -0,0 +1,140 @@
/*
* 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.kotlin.codegen
import com.google.auto.service.AutoService
import com.squareup.kotlinpoet.TypeSpec
import com.squareup.moshi.JsonClass
import me.eugeniomarletti.kotlin.metadata.KotlinMetadataUtils
import me.eugeniomarletti.kotlin.metadata.declaresDefaultValue
import me.eugeniomarletti.kotlin.processing.KotlinAbstractProcessor
import java.io.File
import javax.annotation.processing.ProcessingEnvironment
import javax.annotation.processing.Processor
import javax.annotation.processing.RoundEnvironment
import javax.lang.model.SourceVersion
import javax.lang.model.element.Element
import javax.lang.model.element.TypeElement
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 [JsonClass] factory implementation.
*/
@AutoService(Processor::class)
class JsonClassCodegenProcessor : KotlinAbstractProcessor(), KotlinMetadataUtils {
companion object {
/**
* This annotation processing argument can be specified to have a `@Generated` annotation
* included in the generated code. It is not encouraged unless you need it for static analysis
* reasons and not enabled by default.
*
* Note that this can only be one of the following values:
* * `"javax.annotation.processing.Generated"` (JRE 9+)
* * `"javax.annotation.Generated"` (JRE <9)
*/
const val OPTION_GENERATED = "moshi.generated"
private val POSSIBLE_GENERATED_NAMES = setOf(
"javax.annotation.processing.Generated",
"javax.annotation.Generated"
)
}
private val annotation = JsonClass::class.java
private var generatedType: TypeElement? = null
override fun getSupportedAnnotationTypes() = setOf(annotation.canonicalName)
override fun getSupportedSourceVersion(): SourceVersion = SourceVersion.latest()
override fun getSupportedOptions() = setOf(OPTION_GENERATED)
override fun init(processingEnv: ProcessingEnvironment) {
super.init(processingEnv)
generatedType = processingEnv.options[OPTION_GENERATED]?.let {
if (it !in POSSIBLE_GENERATED_NAMES) {
throw IllegalArgumentException("Invalid option value for $OPTION_GENERATED. Found $it, " +
"allowable values are $POSSIBLE_GENERATED_NAMES.")
}
processingEnv.elementUtils.getTypeElement(it)
}
}
override fun process(annotations: Set<TypeElement>, roundEnv: RoundEnvironment): Boolean {
for (type in roundEnv.getElementsAnnotatedWith(annotation)) {
val jsonClass = type.getAnnotation(annotation)
if (jsonClass.generateAdapter) {
val generator = adapterGenerator(type) ?: continue
generator.generateAndWrite(generatedType)
}
}
return true
}
private fun adapterGenerator(element: Element): AdapterGenerator? {
val type = TargetType.get(messager, elementUtils, typeUtils, element) ?: return null
val properties = mutableMapOf<String, PropertyGenerator>()
for (property in type.properties.values) {
val generator = property.generator(messager)
if (generator != null) {
properties[property.name] = generator
}
}
for ((name, parameter) in type.constructor.parameters) {
if (type.properties[parameter.name] == null && !parameter.proto.declaresDefaultValue) {
messager.printMessage(
ERROR, "No property for required constructor parameter $name", parameter.element)
return null
}
}
// Sort properties so that those with constructor parameters come first.
val sortedProperties = properties.values.sortedBy {
if (it.hasConstructorParameter) {
it.target.parameterIndex
} else {
Integer.MAX_VALUE
}
}
return AdapterGenerator(type, sortedProperties)
}
private fun AdapterGenerator.generateAndWrite(generatedOption: TypeElement?) {
val fileSpec = generateFile(messager, generatedOption)
val adapterName = fileSpec.members.filterIsInstance<TypeSpec>().first().name!!
val outputDir = generatedDir ?: mavenGeneratedDir(adapterName)
fileSpec.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() }
}
}

View File

@@ -0,0 +1,57 @@
/*
* 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.kotlin.codegen
import com.squareup.kotlinpoet.BOOLEAN
import com.squareup.kotlinpoet.NameAllocator
import com.squareup.kotlinpoet.PropertySpec
/** Generates functions to encode and decode a property as JSON. */
internal class PropertyGenerator(val target: TargetProperty) {
val delegateKey = target.delegateKey()
val name = target.name
val jsonName = target.jsonName()
val hasDefault = target.hasDefault
lateinit var localName: String
lateinit var localIsPresentName: String
val isRequired get() = !delegateKey.nullable && !hasDefault
val hasConstructorParameter get() = target.parameterIndex != -1
/** We prefer to use 'null' to mean absent, but for some properties those are distinct. */
val differentiateAbsentFromNull get() = delegateKey.nullable && hasDefault
fun allocateNames(nameAllocator: NameAllocator) {
localName = nameAllocator.newName(name)
localIsPresentName = nameAllocator.newName("${name}Set")
}
fun generateLocalProperty(): PropertySpec {
return PropertySpec.builder(localName, target.type.asNullable())
.mutable(true)
.initializer("null")
.build()
}
fun generateLocalIsPresentProperty(): PropertySpec {
return PropertySpec.builder(localIsPresentName, BOOLEAN)
.mutable(true)
.initializer("false")
.build()
}
}

View File

@@ -0,0 +1,63 @@
/*
* 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.kotlin.codegen
import me.eugeniomarletti.kotlin.metadata.KotlinClassMetadata
import me.eugeniomarletti.kotlin.metadata.isPrimary
import me.eugeniomarletti.kotlin.metadata.jvm.getJvmConstructorSignature
import me.eugeniomarletti.kotlin.metadata.shadow.metadata.ProtoBuf.Constructor
import javax.lang.model.element.ElementKind
import javax.lang.model.element.ExecutableElement
import javax.lang.model.util.Elements
/** A constructor in user code that should be called by generated code. */
internal data class TargetConstructor(
val element: ExecutableElement,
val proto: Constructor,
val parameters: Map<String, TargetParameter>
) {
companion object {
fun primary(metadata: KotlinClassMetadata, elements: Elements): TargetConstructor {
val (nameResolver, classProto) = metadata.data
// todo allow custom constructor
val proto = classProto.constructorList
.single { it.isPrimary }
val constructorJvmSignature = proto.getJvmConstructorSignature(
nameResolver, classProto.typeTable)
val element = classProto.fqName
.let(nameResolver::getString)
.replace('/', '.')
.let(elements::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 = mutableMapOf<String, TargetParameter>()
for (parameter in proto.valueParameterList) {
val name = nameResolver.getString(parameter.name)
val index = proto.valueParameterList.indexOf(parameter)
parameters[name] = TargetParameter(name, parameter, index, element.parameters[index])
}
return TargetConstructor(element, proto, parameters)
}
}
}

View File

@@ -0,0 +1,27 @@
/*
* 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.kotlin.codegen
import me.eugeniomarletti.kotlin.metadata.shadow.metadata.ProtoBuf.ValueParameter
import javax.lang.model.element.VariableElement
/** A parameter in user code that should be populated by generated code. */
internal data class TargetParameter(
val name: String,
val proto: ValueParameter,
val index: Int,
val element: VariableElement
)

View File

@@ -0,0 +1,135 @@
/*
* 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.kotlin.codegen
import com.google.auto.common.AnnotationMirrors
import com.squareup.kotlinpoet.TypeName
import com.squareup.moshi.Json
import com.squareup.moshi.JsonQualifier
import me.eugeniomarletti.kotlin.metadata.declaresDefaultValue
import me.eugeniomarletti.kotlin.metadata.hasSetter
import me.eugeniomarletti.kotlin.metadata.shadow.metadata.ProtoBuf.Property
import me.eugeniomarletti.kotlin.metadata.shadow.metadata.ProtoBuf.Visibility.INTERNAL
import me.eugeniomarletti.kotlin.metadata.shadow.metadata.ProtoBuf.Visibility.PROTECTED
import me.eugeniomarletti.kotlin.metadata.shadow.metadata.ProtoBuf.Visibility.PUBLIC
import me.eugeniomarletti.kotlin.metadata.visibility
import javax.annotation.processing.Messager
import javax.lang.model.element.AnnotationMirror
import javax.lang.model.element.Element
import javax.lang.model.element.ExecutableElement
import javax.lang.model.element.Modifier
import javax.lang.model.element.VariableElement
import javax.tools.Diagnostic
/** A property in user code that maps to JSON. */
internal data class TargetProperty(
val name: String,
val type: TypeName,
private val proto: Property,
private val parameter: TargetParameter?,
private val annotationHolder: ExecutableElement?,
private val field: VariableElement?,
private val setter: ExecutableElement?,
private val getter: ExecutableElement?
) {
val parameterIndex get() = parameter?.index ?: -1
val hasDefault get() = parameter?.proto?.declaresDefaultValue ?: true
private val isTransient get() = field != null && Modifier.TRANSIENT in field.modifiers
private val element get() = field ?: setter ?: getter!!
private val isSettable get() = proto.hasSetter || parameter != null
private val isVisible: Boolean
get() {
return proto.visibility == INTERNAL
|| proto.visibility == PROTECTED
|| proto.visibility == PUBLIC
}
/**
* Returns a generator for this property, or null if either there is an error and this property
* cannot be used with code gen, or if no codegen is necessary for this property.
*/
fun generator(messager: Messager): PropertyGenerator? {
if (!isVisible) {
messager.printMessage(Diagnostic.Kind.ERROR, "property ${this} is not visible", element)
return null
}
if (isTransient) {
if (!hasDefault) {
messager.printMessage(
Diagnostic.Kind.ERROR, "No default value for transient property ${this}", element)
return null
}
return null // This property is transient and has a default value. Ignore it.
}
if (!isSettable) {
return null // This property is not settable. Ignore it.
}
return PropertyGenerator(this)
}
fun delegateKey() = DelegateKey(type, jsonQualifiers())
/** Returns the JsonQualifiers on the field and parameter of this property. */
private fun jsonQualifiers(): Set<AnnotationMirror> {
val elementQualifiers = element.qualifiers
val annotationHolderQualifiers = annotationHolder.qualifiers
val parameterQualifiers = parameter?.element.qualifiers
// TODO(jwilson): union the qualifiers somehow?
return when {
elementQualifiers.isNotEmpty() -> elementQualifiers
annotationHolderQualifiers.isNotEmpty() -> annotationHolderQualifiers
parameterQualifiers.isNotEmpty() -> parameterQualifiers
else -> setOf()
}
}
private val Element?.qualifiers: Set<AnnotationMirror>
get() {
if (this == null) return setOf()
return AnnotationMirrors.getAnnotatedAnnotations(this, JsonQualifier::class.java)
}
/** Returns the @Json name of this property, or this property's name if none is provided. */
fun jsonName(): String {
val fieldJsonName = element.jsonName
val annotationHolderJsonName = annotationHolder.jsonName
val parameterJsonName = parameter?.element.jsonName
return when {
fieldJsonName != null -> fieldJsonName
annotationHolderJsonName != null -> annotationHolderJsonName
parameterJsonName != null -> parameterJsonName
else -> name
}
}
private val Element?.jsonName: String?
get() {
if (this == null) return null
return getAnnotation(Json::class.java)?.name
}
override fun toString() = name
}

View File

@@ -0,0 +1,224 @@
/*
* 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.kotlin.codegen
import com.squareup.kotlinpoet.ClassName
import com.squareup.kotlinpoet.KModifier
import com.squareup.kotlinpoet.ParameterizedTypeName
import com.squareup.kotlinpoet.TypeVariableName
import com.squareup.kotlinpoet.asClassName
import com.squareup.kotlinpoet.asTypeName
import me.eugeniomarletti.kotlin.metadata.KotlinClassMetadata
import me.eugeniomarletti.kotlin.metadata.KotlinMetadata
import me.eugeniomarletti.kotlin.metadata.classKind
import me.eugeniomarletti.kotlin.metadata.getPropertyOrNull
import me.eugeniomarletti.kotlin.metadata.isInnerClass
import me.eugeniomarletti.kotlin.metadata.kotlinMetadata
import me.eugeniomarletti.kotlin.metadata.modality
import me.eugeniomarletti.kotlin.metadata.shadow.metadata.ProtoBuf.Class
import me.eugeniomarletti.kotlin.metadata.shadow.metadata.ProtoBuf.Modality.ABSTRACT
import me.eugeniomarletti.kotlin.metadata.shadow.metadata.ProtoBuf.TypeParameter
import me.eugeniomarletti.kotlin.metadata.shadow.metadata.ProtoBuf.Visibility.INTERNAL
import me.eugeniomarletti.kotlin.metadata.shadow.metadata.ProtoBuf.Visibility.LOCAL
import me.eugeniomarletti.kotlin.metadata.shadow.metadata.ProtoBuf.Visibility.PUBLIC
import me.eugeniomarletti.kotlin.metadata.shadow.metadata.deserialization.NameResolver
import me.eugeniomarletti.kotlin.metadata.shadow.util.capitalizeDecapitalize.decapitalizeAsciiOnly
import me.eugeniomarletti.kotlin.metadata.visibility
import javax.annotation.processing.Messager
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.element.VariableElement
import javax.lang.model.util.Elements
import javax.lang.model.util.Types
import javax.tools.Diagnostic.Kind.ERROR
/** A user type that should be decoded and encoded by generated code. */
internal data class TargetType(
val proto: Class,
val element: TypeElement,
val constructor: TargetConstructor,
val properties: Map<String, TargetProperty>,
val typeVariables: List<TypeVariableName>
) {
val name = element.className
val hasCompanionObject = proto.hasCompanionObjectName()
companion object {
private val OBJECT_CLASS = ClassName("java.lang", "Object")
/** Returns a target type for `element`, or null if it cannot be used with code gen. */
fun get(messager: Messager, elements: Elements, types: Types, element: Element): TargetType? {
val typeMetadata: KotlinMetadata? = element.kotlinMetadata
if (element !is TypeElement || typeMetadata !is KotlinClassMetadata) {
messager.printMessage(
ERROR, "@JsonClass can't be applied to $element: must be a Kotlin class", element)
return null
}
val proto = typeMetadata.data.classProto
when {
proto.classKind == Class.Kind.ENUM_CLASS -> {
messager.printMessage(
ERROR, "@JsonClass can't be applied to $element: must not be an enum class", element)
return null
}
proto.classKind != Class.Kind.CLASS -> {
messager.printMessage(
ERROR, "@JsonClass can't be applied to $element: must be a Kotlin class", element)
return null
}
proto.isInnerClass -> {
messager.printMessage(
ERROR, "@JsonClass can't be applied to $element: must not be an inner class", element)
return null
}
proto.modality == ABSTRACT -> {
messager.printMessage(
ERROR, "@JsonClass can't be applied to $element: must not be abstract", element)
return null
}
proto.visibility == LOCAL -> {
messager.printMessage(
ERROR, "@JsonClass can't be applied to $element: must not be local", element)
return null
}
}
val typeVariables = genericTypeNames(proto, typeMetadata.data.nameResolver)
val appliedType = AppliedType.get(element)
val constructor = TargetConstructor.primary(typeMetadata, elements)
if (constructor.proto.visibility != INTERNAL && constructor.proto.visibility != PUBLIC) {
messager.printMessage(ERROR, "@JsonClass can't be applied to $element: " +
"primary constructor is not internal or public", element)
return null
}
val properties = mutableMapOf<String, TargetProperty>()
for (supertype in appliedType.supertypes(types)) {
if (supertype.element.asClassName() == OBJECT_CLASS) {
continue // Don't load properties for java.lang.Object.
}
if (supertype.element.kind != ElementKind.CLASS) {
continue // Don't load properties for interface types.
}
if (supertype.element.kotlinMetadata == null) {
messager.printMessage(ERROR,
"@JsonClass can't be applied to $element: supertype $supertype is not a Kotlin type",
element)
return null
}
val supertypeProperties = declaredProperties(
supertype.element, supertype.resolver, constructor)
for ((name, property) in supertypeProperties) {
properties.putIfAbsent(name, property)
}
}
return TargetType(proto, element, constructor, properties, typeVariables)
}
/** Returns the properties declared by `typeElement`. */
private fun declaredProperties(
typeElement: TypeElement,
typeResolver: TypeResolver,
constructor: TargetConstructor
): Map<String, TargetProperty> {
val typeMetadata: KotlinClassMetadata = typeElement.kotlinMetadata as KotlinClassMetadata
val nameResolver = typeMetadata.data.nameResolver
val classProto = typeMetadata.data.classProto
val annotationHolders = mutableMapOf<String, ExecutableElement>()
val fields = mutableMapOf<String, VariableElement>()
val setters = mutableMapOf<String, ExecutableElement>()
val getters = mutableMapOf<String, ExecutableElement>()
for (element in typeElement.enclosedElements) {
if (element is VariableElement) {
fields[element.name] = element
} else if (element is ExecutableElement) {
when {
element.name.startsWith("get") -> {
val name = element.name.substring("get".length).decapitalizeAsciiOnly()
getters[name] = element
}
element.name.startsWith("is") -> {
val name = element.name.substring("is".length).decapitalizeAsciiOnly()
getters[name] = element
}
element.name.startsWith("set") -> {
val name = element.name.substring("set".length).decapitalizeAsciiOnly()
setters[name] = element
}
}
val propertyProto = typeMetadata.data.getPropertyOrNull(element)
if (propertyProto != null) {
val name = nameResolver.getString(propertyProto.name)
annotationHolders[name] = element
}
}
}
val result = mutableMapOf<String, TargetProperty>()
for (property in classProto.propertyList) {
val name = nameResolver.getString(property.name)
val type = typeResolver.resolve(property.returnType.asTypeName(
nameResolver, classProto::getTypeParameter, true))
result[name] = TargetProperty(name, type, property, constructor.parameters[name],
annotationHolders[name], fields[name], setters[name], getters[name])
}
return result
}
private val Element.className: ClassName
get() {
val typeName = asType().asTypeName()
return when (typeName) {
is ClassName -> typeName
is ParameterizedTypeName -> typeName.rawType
else -> throw IllegalStateException("unexpected TypeName: ${typeName::class}")
}
}
private val Element.name get() = simpleName.toString()
private fun genericTypeNames(proto: Class, nameResolver: NameResolver): List<TypeVariableName> {
return proto.typeParameterList.map {
TypeVariableName(
name = nameResolver.getString(it.name),
bounds = *(it.upperBoundList
.map { it.asTypeName(nameResolver, proto::getTypeParameter) }
.toTypedArray()),
variance = it.varianceModifier)
.reified(it.reified)
}
}
private val TypeParameter.varianceModifier: KModifier?
get() {
return variance.asKModifier().let {
// We don't redeclare out variance here
if (it == KModifier.OUT) {
null
} else {
it
}
}
}
}
}

View File

@@ -0,0 +1,105 @@
/*
* 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.kotlin.codegen
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.INT
import com.squareup.kotlinpoet.LONG
import com.squareup.kotlinpoet.ParameterizedTypeName
import com.squareup.kotlinpoet.SHORT
import com.squareup.kotlinpoet.TypeName
import com.squareup.kotlinpoet.TypeVariableName
import com.squareup.kotlinpoet.WildcardTypeName
import com.squareup.kotlinpoet.asTypeName
import com.squareup.moshi.Types
/**
* Renders literals like `Types.newParameterizedType(List::class.java, String::class.java)`.
* Rendering is pluggable so that type variables can either be resolved or emitted as other code
* blocks.
*/
abstract class TypeRenderer {
abstract fun renderTypeVariable(typeVariable: TypeVariableName): CodeBlock
fun render(typeName: TypeName): CodeBlock {
if (typeName.nullable) {
return render(typeName.asNonNullable())
}
return when (typeName) {
is ClassName -> CodeBlock.of("%T::class.java", typeName)
is ParameterizedTypeName -> {
// If it's an Array type, we shortcut this to return Types.arrayOf()
if (typeName.rawType == ARRAY) {
CodeBlock.of("%T.arrayOf(%L)",
Types::class,
render(typeName.typeArguments[0].objectType()))
} else {
val placeholders = typeName.typeArguments.joinToString(", ") { "%L" }
CodeBlock.of(
"%T.newParameterizedType(%T::class.java, $placeholders)",
Types::class,
typeName.rawType.objectType(),
*(typeName.typeArguments.map { render(it.objectType()) }.toTypedArray()))
}
}
is WildcardTypeName -> {
val target: TypeName
val method: String
when {
typeName.lowerBounds.size == 1 -> {
target = typeName.lowerBounds[0]
method = "supertypeOf"
}
typeName.upperBounds.size == 1 -> {
target = typeName.upperBounds[0]
method = "subtypeOf"
}
else -> throw IllegalArgumentException(
"Unrepresentable wildcard type. Cannot have more than one bound: $typeName")
}
CodeBlock.of("%T.%L(%T::class.java)", Types::class, method, target)
}
is TypeVariableName -> renderTypeVariable(typeName)
else -> throw IllegalArgumentException("Unrepresentable type: $typeName")
}
}
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
}
}
}

View File

@@ -0,0 +1,63 @@
/*
* 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.kotlin.codegen
import com.squareup.kotlinpoet.ClassName
import com.squareup.kotlinpoet.ParameterizedTypeName
import com.squareup.kotlinpoet.TypeName
import com.squareup.kotlinpoet.TypeVariableName
import com.squareup.kotlinpoet.WildcardTypeName
/**
* Resolves type parameters against a type declaration. Use this to fill in type variables with
* their actual type parameters.
*/
open class TypeResolver {
open fun resolveTypeVariable(typeVariable: TypeVariableName): TypeName = typeVariable
fun resolve(typeName: TypeName): TypeName {
return when (typeName) {
is ClassName -> typeName
is ParameterizedTypeName -> {
ParameterizedTypeName.get(
typeName.rawType, *(typeName.typeArguments.map { resolve(it) }.toTypedArray()))
.asNullableIf(typeName.nullable)
}
is WildcardTypeName -> {
when {
typeName.lowerBounds.size == 1 -> {
WildcardTypeName.supertypeOf(resolve(typeName.lowerBounds[0]))
.asNullableIf(typeName.nullable)
}
typeName.upperBounds.size == 1 -> {
WildcardTypeName.subtypeOf(resolve(typeName.upperBounds[0]))
.asNullableIf(typeName.nullable)
}
else -> {
throw IllegalArgumentException(
"Unrepresentable wildcard type. Cannot have more than one bound: $typeName")
}
}
}
is TypeVariableName -> resolveTypeVariable(typeName)
else -> throw IllegalArgumentException("Unrepresentable type: $typeName")
}
}
}

View File

@@ -0,0 +1,32 @@
/*
* 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.kotlin.codegen
import com.squareup.kotlinpoet.ClassName
import com.squareup.kotlinpoet.ParameterizedTypeName
import com.squareup.kotlinpoet.TypeName
internal fun TypeName.rawType(): ClassName {
return when (this) {
is ClassName -> this
is ParameterizedTypeName -> rawType
else -> throw IllegalArgumentException("Cannot get raw type from $this")
}
}
internal fun TypeName.asNullableIf(condition: Boolean): TypeName {
return if (condition) asNullable() else this
}

View File

@@ -0,0 +1,128 @@
/*
* 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.kotlin.codegen
import com.squareup.kotlinpoet.ANY
import com.squareup.kotlinpoet.ClassName
import com.squareup.kotlinpoet.KModifier
import com.squareup.kotlinpoet.ParameterizedTypeName
import com.squareup.kotlinpoet.TypeName
import com.squareup.kotlinpoet.TypeVariableName
import com.squareup.kotlinpoet.WildcardTypeName
import me.eugeniomarletti.kotlin.metadata.shadow.metadata.ProtoBuf.Type
import me.eugeniomarletti.kotlin.metadata.shadow.metadata.ProtoBuf.TypeParameter
import me.eugeniomarletti.kotlin.metadata.shadow.metadata.ProtoBuf.TypeParameter.Variance
import me.eugeniomarletti.kotlin.metadata.shadow.metadata.deserialization.NameResolver
internal fun TypeParameter.asTypeName(
nameResolver: NameResolver,
getTypeParameter: (index: Int) -> TypeParameter,
resolveAliases: Boolean = false
): TypeName {
return TypeVariableName(
name = nameResolver.getString(name),
bounds = *(upperBoundList.map {
it.asTypeName(nameResolver, getTypeParameter, resolveAliases)
}
.toTypedArray()),
variance = variance.asKModifier()
)
}
internal fun TypeParameter.Variance.asKModifier(): KModifier? {
return when (this) {
Variance.IN -> KModifier.IN
Variance.OUT -> KModifier.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!**
*/
internal fun Type.asTypeName(
nameResolver: NameResolver,
getTypeParameter: (index: Int) -> TypeParameter,
resolveAliases: Boolean = false
): TypeName {
val argumentList = when {
hasAbbreviatedType() -> abbreviatedType.argumentList
else -> argumentList
}
if (hasFlexibleUpperBound()) {
return WildcardTypeName.subtypeOf(
flexibleUpperBound.asTypeName(nameResolver, getTypeParameter, resolveAliases))
.asNullableIf(nullable)
} else if (hasOuterType()) {
return WildcardTypeName.supertypeOf(
outerType.asTypeName(nameResolver, getTypeParameter, resolveAliases))
.asNullableIf(nullable)
}
val realType = when {
hasTypeParameter() -> return getTypeParameter(typeParameter)
.asTypeName(nameResolver, getTypeParameter, resolveAliases)
.asNullableIf(nullable)
hasTypeParameterName() -> typeParameterName
hasAbbreviatedType() && !resolveAliases -> abbreviatedType.typeAliasName
else -> className
}
var typeName: TypeName =
ClassName.bestGuess(nameResolver.getString(realType)
.replace("/", "."))
if (argumentList.isNotEmpty()) {
val remappedArgs: Array<TypeName> = argumentList.map { argumentType ->
val nullableProjection = if (argumentType.hasProjection()) {
argumentType.projection
} else null
if (argumentType.hasType()) {
argumentType.type.asTypeName(nameResolver, getTypeParameter, resolveAliases)
.let { argumentTypeName ->
nullableProjection?.let { projection ->
when (projection) {
Type.Argument.Projection.IN -> WildcardTypeName.supertypeOf(argumentTypeName)
Type.Argument.Projection.OUT -> {
if (argumentTypeName == ANY) {
// This becomes a *, which we actually don't want here.
// List<Any> works with List<*>, but List<*> doesn't work with List<Any>
argumentTypeName
} else {
WildcardTypeName.subtypeOf(argumentTypeName)
}
}
Type.Argument.Projection.STAR -> WildcardTypeName.subtypeOf(ANY)
Type.Argument.Projection.INV -> TODO("INV projection is unsupported")
}
} ?: argumentTypeName
}
} else {
WildcardTypeName.subtypeOf(ANY)
}
}.toTypedArray()
typeName = ParameterizedTypeName.get(typeName as ClassName, *remappedArgs)
}
return typeName.asNullableIf(nullable)
}

View File

@@ -0,0 +1,21 @@
/*
* 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.kotlin.codegen;
/** For {@link JsonClassCodegenProcessorTest#extendJavaType}. */
public class JavaSuperclass {
public int a = 1;
}

View File

@@ -0,0 +1,381 @@
/*
* 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.kotlin.codegen
import org.assertj.core.api.Assertions.assertThat
import org.jetbrains.kotlin.cli.common.ExitCode
import org.junit.Ignore
import org.junit.Rule
import org.junit.Test
import org.junit.rules.TemporaryFolder
import javax.annotation.processing.Processor
/** Execute kotlinc to confirm that either files are generated or errors are printed. */
class JsonClassCodegenProcessorTest {
@Rule @JvmField var temporaryFolder: TemporaryFolder = TemporaryFolder()
@Test fun privateConstructor() {
val call = KotlinCompilerCall(temporaryFolder.root)
call.inheritClasspath = true
call.addService(Processor::class, JsonClassCodegenProcessor::class)
call.addKt("source.kt", """
|import com.squareup.moshi.JsonClass
|
|@JsonClass(generateAdapter = true)
|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)
| }
|}
|""".trimMargin())
val result = call.execute()
assertThat(result.exitCode).isEqualTo(ExitCode.COMPILATION_ERROR)
assertThat(result.systemErr).contains("constructor is not internal or public")
}
@Test fun privateConstructorParameter() {
val call = KotlinCompilerCall(temporaryFolder.root)
call.inheritClasspath = true
call.addService(Processor::class, JsonClassCodegenProcessor::class)
call.addKt("source.kt", """
|import com.squareup.moshi.JsonClass
|
|@JsonClass(generateAdapter = true)
|class PrivateConstructorParameter(private var a: Int)
|""".trimMargin())
val result = call.execute()
assertThat(result.exitCode).isEqualTo(ExitCode.COMPILATION_ERROR)
assertThat(result.systemErr).contains("property a is not visible")
}
@Test fun privateProperties() {
val call = KotlinCompilerCall(temporaryFolder.root)
call.inheritClasspath = true
call.addService(Processor::class, JsonClassCodegenProcessor::class)
call.addKt("source.kt", """
|import com.squareup.moshi.JsonClass
|
|@JsonClass(generateAdapter = true)
|class PrivateProperties {
| private var a: Int = -1
| private var b: Int = -1
|}
|""".trimMargin())
val result = call.execute()
assertThat(result.exitCode).isEqualTo(ExitCode.COMPILATION_ERROR)
assertThat(result.systemErr).contains("property a is not visible")
}
@Test fun interfacesNotSupported() {
val call = KotlinCompilerCall(temporaryFolder.root)
call.inheritClasspath = true
call.addService(Processor::class, JsonClassCodegenProcessor::class)
call.addKt("source.kt", """
|import com.squareup.moshi.JsonClass
|
|@JsonClass(generateAdapter = true)
|interface Interface
|""".trimMargin())
val result = call.execute()
assertThat(result.exitCode).isEqualTo(ExitCode.COMPILATION_ERROR)
assertThat(result.systemErr).contains(
"error: @JsonClass can't be applied to Interface: must be a Kotlin class")
}
@Test fun abstractClassesNotSupported() {
val call = KotlinCompilerCall(temporaryFolder.root)
call.inheritClasspath = true
call.addService(Processor::class, JsonClassCodegenProcessor::class)
call.addKt("source.kt", """
|import com.squareup.moshi.JsonClass
|
|@JsonClass(generateAdapter = true)
|abstract class AbstractClass(val a: Int)
|""".trimMargin())
val result = call.execute()
assertThat(result.exitCode).isEqualTo(ExitCode.COMPILATION_ERROR)
assertThat(result.systemErr).contains(
"error: @JsonClass can't be applied to AbstractClass: must not be abstract")
}
@Test fun innerClassesNotSupported() {
val call = KotlinCompilerCall(temporaryFolder.root)
call.inheritClasspath = true
call.addService(Processor::class, JsonClassCodegenProcessor::class)
call.addKt("source.kt", """
|import com.squareup.moshi.JsonClass
|
|class Outer {
| @JsonClass(generateAdapter = true)
| inner class InnerClass(val a: Int)
|}
|""".trimMargin())
val result = call.execute()
assertThat(result.exitCode).isEqualTo(ExitCode.COMPILATION_ERROR)
assertThat(result.systemErr).contains(
"error: @JsonClass can't be applied to Outer.InnerClass: must not be an inner class")
}
@Test fun enumClassesNotSupported() {
val call = KotlinCompilerCall(temporaryFolder.root)
call.inheritClasspath = true
call.addService(Processor::class, JsonClassCodegenProcessor::class)
call.addKt("source.kt", """
|import com.squareup.moshi.JsonClass
|
|@JsonClass(generateAdapter = true)
|enum class KotlinEnum {
| A, B
|}
|""".trimMargin())
val result = call.execute()
assertThat(result.exitCode).isEqualTo(ExitCode.COMPILATION_ERROR)
assertThat(result.systemErr).contains(
"error: @JsonClass can't be applied to KotlinEnum: must not be an enum class")
}
// Annotation processors don't get called for local classes, so we don't have the opportunity to
// print an error message. Instead local classes will fail at runtime.
@Ignore
@Test fun localClassesNotSupported() {
val call = KotlinCompilerCall(temporaryFolder.root)
call.inheritClasspath = true
call.addService(Processor::class, JsonClassCodegenProcessor::class)
call.addKt("source.kt", """
|import com.squareup.moshi.JsonClass
|
|fun outer() {
| @JsonClass(generateAdapter = true)
| class LocalClass(val a: Int)
|}
|""".trimMargin())
val result = call.execute()
assertThat(result.exitCode).isEqualTo(ExitCode.COMPILATION_ERROR)
assertThat(result.systemErr).contains(
"error: @JsonClass can't be applied to LocalClass: must not be local")
}
@Test fun objectDeclarationsNotSupported() {
val call = KotlinCompilerCall(temporaryFolder.root)
call.inheritClasspath = true
call.addService(Processor::class, JsonClassCodegenProcessor::class)
call.addKt("source.kt", """
|import com.squareup.moshi.JsonClass
|
|@JsonClass(generateAdapter = true)
|object ObjectDeclaration {
| var a = 5
|}
|""".trimMargin())
val result = call.execute()
assertThat(result.exitCode).isEqualTo(ExitCode.COMPILATION_ERROR)
assertThat(result.systemErr).contains(
"error: @JsonClass can't be applied to ObjectDeclaration: must be a Kotlin class")
}
@Test fun objectExpressionsNotSupported() {
val call = KotlinCompilerCall(temporaryFolder.root)
call.inheritClasspath = true
call.addService(Processor::class, JsonClassCodegenProcessor::class)
call.addKt("source.kt", """
|import com.squareup.moshi.JsonClass
|
|@JsonClass(generateAdapter = true)
|val expression = object : Any() {
| var a = 5
|}
|""".trimMargin())
val result = call.execute()
assertThat(result.exitCode).isEqualTo(ExitCode.COMPILATION_ERROR)
assertThat(result.systemErr).contains(
"error: @JsonClass can't be applied to expression\$annotations(): must be a Kotlin class")
}
@Test fun requiredTransientConstructorParameterFails() {
val call = KotlinCompilerCall(temporaryFolder.root)
call.inheritClasspath = true
call.addService(Processor::class, JsonClassCodegenProcessor::class)
call.addKt("source.kt", """
|import com.squareup.moshi.JsonClass
|
|@JsonClass(generateAdapter = true)
|class RequiredTransientConstructorParameter(@Transient var a: Int)
|""".trimMargin())
val result = call.execute()
assertThat(result.exitCode).isEqualTo(ExitCode.COMPILATION_ERROR)
assertThat(result.systemErr).contains(
"error: No default value for transient property a")
}
@Test fun nonPropertyConstructorParameter() {
val call = KotlinCompilerCall(temporaryFolder.root)
call.inheritClasspath = true
call.addService(Processor::class, JsonClassCodegenProcessor::class)
call.addKt("source.kt", """
|import com.squareup.moshi.JsonClass
|
|@JsonClass(generateAdapter = true)
|class NonPropertyConstructorParameter(a: Int, val b: Int)
|""".trimMargin())
val result = call.execute()
assertThat(result.exitCode).isEqualTo(ExitCode.COMPILATION_ERROR)
assertThat(result.systemErr).contains(
"error: No property for required constructor parameter a")
}
@Test fun badGeneratedAnnotation() {
val call = KotlinCompilerCall(temporaryFolder.root)
call.inheritClasspath = true
call.addService(Processor::class, JsonClassCodegenProcessor::class)
call.kaptArgs[JsonClassCodegenProcessor.OPTION_GENERATED] = "javax.annotation.GeneratedBlerg"
call.addKt("source.kt", """
|import com.squareup.moshi.JsonClass
|
|@JsonClass(generateAdapter = true)
|data class Foo(val a: Int)
|""".trimMargin())
val result = call.execute()
assertThat(result.exitCode).isEqualTo(ExitCode.COMPILATION_ERROR)
assertThat(result.systemErr).contains(
"Invalid option value for ${JsonClassCodegenProcessor.OPTION_GENERATED}")
}
@Test fun multipleErrors() {
val call = KotlinCompilerCall(temporaryFolder.root)
call.inheritClasspath = true
call.addService(Processor::class, JsonClassCodegenProcessor::class)
call.addKt("source.kt", """
|import com.squareup.moshi.JsonClass
|
|@JsonClass(generateAdapter = true)
|class Class1(private var a: Int, private var b: Int)
|
|@JsonClass(generateAdapter = true)
|class Class2(private var c: Int)
|""".trimMargin())
val result = call.execute()
assertThat(result.exitCode).isEqualTo(ExitCode.COMPILATION_ERROR)
assertThat(result.systemErr).contains("property a is not visible")
assertThat(result.systemErr).contains("property b is not visible")
assertThat(result.systemErr).contains("property c is not visible")
}
@Test fun extendPlatformType() {
val call = KotlinCompilerCall(temporaryFolder.root)
call.inheritClasspath = true
call.addService(Processor::class, JsonClassCodegenProcessor::class)
call.addKt("source.kt", """
|import com.squareup.moshi.JsonClass
|import java.util.Date
|
|@JsonClass(generateAdapter = true)
|class ExtendsPlatformClass(var a: Int) : Date()
|""".trimMargin())
val result = call.execute()
assertThat(result.exitCode).isEqualTo(ExitCode.COMPILATION_ERROR)
assertThat(result.systemErr).contains("supertype java.util.Date is not a Kotlin type")
}
@Test fun extendJavaType() {
val call = KotlinCompilerCall(temporaryFolder.root)
call.inheritClasspath = true
call.addService(Processor::class, JsonClassCodegenProcessor::class)
call.addKt("source.kt", """
|import com.squareup.moshi.JsonClass
|import com.squareup.moshi.kotlin.codegen.JavaSuperclass
|
|@JsonClass(generateAdapter = true)
|class ExtendsJavaType(var b: Int) : JavaSuperclass()
|""".trimMargin())
val result = call.execute()
assertThat(result.exitCode).isEqualTo(ExitCode.COMPILATION_ERROR)
assertThat(result.systemErr)
.contains("supertype com.squareup.moshi.kotlin.codegen.JavaSuperclass is not a Kotlin type")
}
@Test
fun nonFieldApplicableQualifier() {
val call = KotlinCompilerCall(temporaryFolder.root)
call.inheritClasspath = true
call.addService(Processor::class, JsonClassCodegenProcessor::class)
call.addKt("source.kt", """
|import com.squareup.moshi.JsonClass
|import com.squareup.moshi.JsonQualifier
|import kotlin.annotation.AnnotationRetention.RUNTIME
|import kotlin.annotation.AnnotationTarget.PROPERTY
|import kotlin.annotation.Retention
|import kotlin.annotation.Target
|
|@Retention(RUNTIME)
|@Target(PROPERTY)
|@JsonQualifier
|annotation class UpperCase
|
|@JsonClass(generateAdapter = true)
|class ClassWithQualifier(@UpperCase val a: Int)
|""".trimMargin())
val result = call.execute()
println(result.systemErr)
assertThat(result.exitCode).isEqualTo(ExitCode.COMPILATION_ERROR)
assertThat(result.systemErr).contains("JsonQualifier @UpperCase must support FIELD target")
}
@Test
fun nonRuntimeQualifier() {
val call = KotlinCompilerCall(temporaryFolder.root)
call.inheritClasspath = true
call.addService(Processor::class, JsonClassCodegenProcessor::class)
call.addKt("source.kt", """
|import com.squareup.moshi.JsonClass
|import com.squareup.moshi.JsonQualifier
|import kotlin.annotation.AnnotationRetention.BINARY
|import kotlin.annotation.AnnotationTarget.FIELD
|import kotlin.annotation.AnnotationTarget.PROPERTY
|import kotlin.annotation.Retention
|import kotlin.annotation.Target
|
|@Retention(BINARY)
|@Target(PROPERTY, FIELD)
|@JsonQualifier
|annotation class UpperCase
|
|@JsonClass(generateAdapter = true)
|class ClassWithQualifier(@UpperCase val a: Int)
|""".trimMargin())
val result = call.execute()
assertThat(result.exitCode).isEqualTo(ExitCode.COMPILATION_ERROR)
assertThat(result.systemErr).contains("JsonQualifier @UpperCase must have RUNTIME retention")
}
}

View File

@@ -0,0 +1,193 @@
/*
* 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.kotlin.codegen
import com.google.common.collect.LinkedHashMultimap
import okio.Buffer
import okio.Okio
import org.jetbrains.kotlin.cli.common.CLITool
import org.jetbrains.kotlin.cli.jvm.K2JVMCompiler
import java.io.File
import java.io.FileOutputStream
import java.io.ObjectOutputStream
import java.io.PrintStream
import java.net.URLClassLoader
import java.net.URLDecoder
import java.util.zip.ZipEntry
import java.util.zip.ZipOutputStream
import kotlin.reflect.KClass
/** Prepares an invocation of the Kotlin compiler. */
class KotlinCompilerCall(var scratchDir: File) {
val sourcesDir = File(scratchDir, "sources")
val classesDir = File(scratchDir, "classes")
val servicesJar = File(scratchDir, "services.jar")
var inheritClasspath = false
val args = mutableListOf<String>()
val kaptArgs = mutableMapOf<String, String>()
val classpath = mutableListOf<String>()
val services = LinkedHashMultimap.create<KClass<*>, KClass<*>>()
/** Adds a source file to be compiled. */
fun addKt(path: String, source: String) {
val sourceFile = File(sourcesDir, path)
sourceFile.parentFile.mkdirs()
Okio.buffer(Okio.sink(sourceFile)).use {
it.writeUtf8(source)
}
}
/** Adds a service like an annotation processor to make available to the compiler. */
fun addService(serviceClass: KClass<*>, implementation: KClass<*>) {
services.put(serviceClass, implementation)
}
fun execute(): KotlinCompilerResult {
val fullArgs = mutableListOf<String>()
fullArgs.addAll(args)
fullArgs.add("-d")
fullArgs.add(classesDir.toString())
val fullClasspath = fullClasspath()
if (fullClasspath.isNotEmpty()) {
fullArgs.add("-classpath")
fullArgs.add(fullClasspath.joinToString(separator = ":"))
}
for (source in sourcesDir.listFiles()) {
fullArgs.add(source.toString())
}
fullArgs.addAll(annotationProcessorArgs())
if (kaptArgs.isNotEmpty()) {
fullArgs.apply {
add("-P")
add("plugin:org.jetbrains.kotlin.kapt3:apoptions=${encodeOptions(kaptArgs)}")
}
}
val systemErrBuffer = Buffer()
val oldSystemErr = System.err
System.setErr(PrintStream(systemErrBuffer.outputStream()))
try {
val exitCode = CLITool.doMainNoExit(K2JVMCompiler(), fullArgs.toTypedArray())
val systemErr = systemErrBuffer.readUtf8()
return KotlinCompilerResult(systemErr, exitCode)
} finally {
System.setErr(oldSystemErr)
}
}
/** Returns arguments necessary to enable and configure kapt3. */
private fun annotationProcessorArgs(): List<String> {
val kaptSourceDir = File(scratchDir, "kapt/sources")
val kaptStubsDir = File(scratchDir, "kapt/stubs")
return listOf(
"-Xplugin=${kapt3Jar()}",
"-P", "plugin:org.jetbrains.kotlin.kapt3:sources=$kaptSourceDir",
"-P", "plugin:org.jetbrains.kotlin.kapt3:classes=$classesDir",
"-P", "plugin:org.jetbrains.kotlin.kapt3:stubs=$kaptStubsDir",
"-P", "plugin:org.jetbrains.kotlin.kapt3:apclasspath=$servicesJar",
"-P", "plugin:org.jetbrains.kotlin.kapt3:correctErrorTypes=true"
)
}
/** Returns the classpath to use when compiling code. */
private fun fullClasspath(): List<String> {
val result = mutableListOf<String>()
result.addAll(classpath)
// Copy over the classpath of the running application.
if (inheritClasspath) {
for (classpathFile in classpathFiles()) {
result.add(classpathFile.toString())
}
}
if (!services.isEmpty) {
writeServicesJar()
result.add(servicesJar.toString())
}
return result.toList()
}
/**
* Generate a .jar file that holds ServiceManager registrations. Necessary because AutoService's
* results might not be visible to this test.
*/
private fun writeServicesJar() {
ZipOutputStream(FileOutputStream(servicesJar)).use { zipOutputStream ->
for (entry in services.asMap()) {
zipOutputStream.putNextEntry(
ZipEntry("META-INF/services/${entry.key.qualifiedName}"))
val serviceFile = Okio.buffer(Okio.sink(zipOutputStream))
for (implementation in entry.value) {
serviceFile.writeUtf8(implementation.qualifiedName)
serviceFile.writeUtf8("\n")
}
serviceFile.emit() // Don't close the entry; that closes the file.
zipOutputStream.closeEntry()
}
}
}
/** Returns the files on the host process' classpath. */
private fun classpathFiles(): List<File> {
val classLoader = JsonClassCodegenProcessorTest::class.java.classLoader
if (classLoader !is URLClassLoader) {
throw UnsupportedOperationException("unable to extract classpath from $classLoader")
}
val result = mutableListOf<File>()
for (url in classLoader.urLs) {
if (url.protocol != "file") {
throw UnsupportedOperationException("unable to handle classpath element $url")
}
result.add(File(URLDecoder.decode(url.path, "UTF-8")))
}
return result.toList()
}
/** Returns the path to the kotlin-annotation-processing .jar file. */
private fun kapt3Jar(): File {
for (file in classpathFiles()) {
if (file.name.startsWith("kotlin-annotation-processing-embeddable")) return file
}
throw IllegalStateException("no kotlin-annotation-processing-embeddable jar on classpath:\n " +
"${classpathFiles().joinToString(separator = "\n ")}}")
}
/**
* Base64 encodes a mapping of annotation processor args for kapt, as specified by
* https://kotlinlang.org/docs/reference/kapt.html#apjavac-options-encoding
*/
private fun encodeOptions(options: Map<String, String>): String {
val buffer = Buffer()
ObjectOutputStream(buffer.outputStream()).use { oos ->
oos.writeInt(options.size)
for ((key, value) in options.entries) {
oos.writeUTF(key)
oos.writeUTF(value)
}
}
return buffer.readByteString().base64()
}
}

View File

@@ -0,0 +1,23 @@
/*
* 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.kotlin.codegen
import org.jetbrains.kotlin.cli.common.ExitCode
class KotlinCompilerResult(
val systemErr: String,
var exitCode: ExitCode
)

View File

@@ -0,0 +1,50 @@
/*
* 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.kotlin.codegen
import com.google.common.truth.Truth.assertThat
import com.squareup.kotlinpoet.ParameterizedTypeName
import com.squareup.kotlinpoet.WildcardTypeName
import com.squareup.kotlinpoet.asClassName
import com.squareup.moshi.kotlin.codegen.TypeResolver
import org.junit.Test
class TypeResolverTest {
private val resolver = TypeResolver()
@Test
fun ensureClassNameNullabilityIsPreserved() {
assertThat(resolver.resolve(Int::class.asClassName().asNullable()).nullable).isTrue()
}
@Test
fun ensureParameterizedNullabilityIsPreserved() {
val nullableTypeName = ParameterizedTypeName.get(
List::class.asClassName(),
String::class.asClassName())
.asNullable()
assertThat(resolver.resolve(nullableTypeName).nullable).isTrue()
}
@Test
fun ensureWildcardNullabilityIsPreserved() {
val nullableTypeName = WildcardTypeName.subtypeOf(List::class.asClassName())
.asNullable()
assertThat(resolver.resolve(nullableTypeName).nullable).isTrue()
}
}

143
kotlin/tests/pom.xml Normal file
View File

@@ -0,0 +1,143 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>com.squareup.moshi</groupId>
<artifactId>moshi-parent</artifactId>
<version>1.6.0-SNAPSHOT</version>
<relativePath>../../pom.xml</relativePath>
</parent>
<artifactId>moshi-kotlin-tests</artifactId>
<dependencies>
<dependency>
<groupId>com.squareup.moshi</groupId>
<artifactId>moshi</artifactId>
<version>${project.version}</version>
</dependency>
<dependency>
<groupId>org.jetbrains.kotlin</groupId>
<artifactId>kotlin-stdlib</artifactId>
</dependency>
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.assertj</groupId>
<artifactId>assertj-core</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<artifactId>kotlin-maven-plugin</artifactId>
<groupId>org.jetbrains.kotlin</groupId>
<version>${kotlin.version}</version>
<executions>
<execution>
<id>kapt</id>
<goals>
<goal>kapt</goal>
</goals>
<configuration>
<sourceDirs>
<sourceDir>src/main/kotlin</sourceDir>
</sourceDirs>
<annotationProcessorPaths>
<annotationProcessorPath>
<groupId>com.squareup.moshi</groupId>
<artifactId>moshi-kotlin-codegen</artifactId>
<version>${project.version}</version>
</annotationProcessorPath>
</annotationProcessorPaths>
</configuration>
</execution>
<execution>
<id>compile</id>
<goals>
<goal>compile</goal>
</goals>
<configuration>
<sourceDirs>
<sourceDir>src/main/kotlin</sourceDir>
<sourceDir>src/main/java</sourceDir>
</sourceDirs>
</configuration>
</execution>
<execution>
<id>test-kapt</id>
<goals>
<goal>test-kapt</goal>
</goals>
<configuration>
<sourceDirs>
<sourceDir>src/test/kotlin</sourceDir>
<sourceDir>src/test/java</sourceDir>
</sourceDirs>
<annotationProcessorPaths>
<annotationProcessorPath>
<groupId>com.squareup.moshi</groupId>
<artifactId>moshi-kotlin-codegen</artifactId>
<version>${project.version}</version>
</annotationProcessorPath>
</annotationProcessorPaths>
</configuration>
</execution>
<execution>
<id>test-compile</id>
<goals>
<goal>test-compile</goal>
</goals>
<configuration>
<sourceDirs>
<sourceDir>src/test/kotlin</sourceDir>
<sourceDir>src/test/java</sourceDir>
<sourceDir>target/generated-sources/kapt/test</sourceDir>
</sourceDirs>
</configuration>
</execution>
</executions>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<version>3.5.1</version>
<configuration>
<proc>none</proc>
<source>1.6</source>
<target>1.6</target>
</configuration>
<executions>
<!-- Replacing default-compile as it is treated specially by maven -->
<execution>
<id>default-compile</id>
<phase>none</phase>
</execution>
<!-- Replacing default-testCompile as it is treated specially by maven -->
<execution>
<id>default-testCompile</id>
<phase>none</phase>
</execution>
<execution>
<id>java-compile</id>
<phase>compile</phase>
<goals>
<goal>compile</goal>
</goals>
</execution>
<execution>
<id>java-test-compile</id>
<phase>test-compile</phase>
<goals> <goal>testCompile</goal> </goals>
</execution>
</executions>
</plugin>
</plugins>
</build>
</project>

View File

@@ -0,0 +1,840 @@
/*
* 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.kotlin.codgen
import com.squareup.moshi.FromJson
import com.squareup.moshi.Json
import com.squareup.moshi.JsonClass
import com.squareup.moshi.JsonDataException
import com.squareup.moshi.JsonQualifier
import com.squareup.moshi.Moshi
import com.squareup.moshi.ToJson
import com.squareup.moshi.Types
import org.assertj.core.api.Assertions.assertThat
import org.intellij.lang.annotations.Language
import org.junit.Assert.fail
import org.junit.Ignore
import org.junit.Test
import java.util.Locale
class GeneratedAdaptersTest {
private val moshi = Moshi.Builder().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)
}
@JsonClass(generateAdapter = true)
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)
}
@JsonClass(generateAdapter = true)
data class DefaultValues(
val foo: String,
val bar: String = "",
val nullableBar: String? = null,
val bazList: List<String> = 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)
}
@JsonClass(generateAdapter = true)
data class NullableArray(val data: Array<String?>)
@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)
}
@JsonClass(generateAdapter = true)
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")
}
}
@JsonClass(generateAdapter = true)
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)
}
@JsonClass(generateAdapter = true)
data class SpecialCollections(
val mutableList: MutableList<String>,
val mutableSet: MutableSet<String>,
val mutableMap: MutableMap<String, String>,
val immutableList: List<String>,
val immutableSet: Set<String>,
val immutableMap: Map<String, String>
)
@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)
}
@JsonClass(generateAdapter = true)
data class MutableProperties(
val immutableProperty: String,
var mutableProperty: String,
val immutableMutableList: MutableList<String>,
val immutableImmutableList: List<String>,
var mutableMutableList: MutableList<String>,
var mutableImmutableList: List<String>,
val nullableImmutableProperty: String?,
var nullableMutableProperty: String?,
val nullableImmutableMutableList: MutableList<String>?,
val nullableImmutableImmutableList: List<String>?,
var nullableMutableMutableList: MutableList<String>?,
var nullableMutableImmutableList: List<String>
)
@Test
fun nullableTypeParams() {
val adapter = moshi.adapter<NullableTypeParams<Int>>(
Types.newParameterizedType(NullableTypeParams::class.java, Int::class.javaObjectType))
val nullSerializing = adapter.serializeNulls()
val nullableTypeParams = NullableTypeParams<Int>(
listOf("foo", null, "bar"),
setOf("foo", null, "bar"),
mapOf("foo" to "bar", "baz" to null),
null,
1
)
val noNullsTypeParams = NullableTypeParams<Int>(
nullableTypeParams.nullableList,
nullableTypeParams.nullableSet,
nullableTypeParams.nullableMap.filterValues { it != null },
null,
1
)
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)
}
@Test fun doNotGenerateAdapter() {
try {
Class.forName("${GeneratedAdaptersTest::class.java.name}_DoNotGenerateAdapterJsonAdapter")
fail("found a generated adapter for a type that shouldn't have one")
} catch (expected: ClassNotFoundException) {
}
}
@JsonClass(generateAdapter = false)
data class DoNotGenerateAdapter(val foo: String)
@Test fun constructorParameters() {
val moshi = Moshi.Builder().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)
}
@JsonClass(generateAdapter = true)
class ConstructorParameters(var a: Int, var b: Int)
@Test fun properties() {
val moshi = Moshi.Builder().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)
}
@JsonClass(generateAdapter = true)
class Properties {
var a: Int = -1
var b: Int = -1
}
@Test fun constructorParametersAndProperties() {
val moshi = Moshi.Builder().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)
}
@JsonClass(generateAdapter = true)
class ConstructorParametersAndProperties(var a: Int) {
var b: Int = -1
}
@Test fun immutableConstructorParameters() {
val moshi = Moshi.Builder().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)
}
@JsonClass(generateAdapter = true)
class ImmutableConstructorParameters(val a: Int, val b: Int)
@Test fun immutableProperties() {
val moshi = Moshi.Builder().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)
}
@JsonClass(generateAdapter = true)
class ImmutableProperties(a: Int, b: Int) {
val a = a
val b = b
}
@Test fun constructorDefaults() {
val moshi = Moshi.Builder().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)
}
@JsonClass(generateAdapter = true)
class ConstructorDefaultValues(var a: Int = -1, var b: Int = -2)
@Test fun requiredValueAbsent() {
val moshi = Moshi.Builder().build()
val jsonAdapter = moshi.adapter(RequiredValueAbsent::class.java)
try {
jsonAdapter.fromJson("""{"a":4}""")
fail()
} catch(expected: JsonDataException) {
assertThat(expected).hasMessage("Required property 'b' missing at \$")
}
}
@JsonClass(generateAdapter = true)
class RequiredValueAbsent(var a: Int = 3, var b: Int)
@Test fun nonNullConstructorParameterCalledWithNullFailsWithJsonDataException() {
val moshi = Moshi.Builder().build()
val jsonAdapter = moshi.adapter(HasNonNullConstructorParameter::class.java)
try {
jsonAdapter.fromJson("{\"a\":null}")
fail()
} catch (expected: JsonDataException) {
assertThat(expected).hasMessage("Unexpected null at \$.a")
}
}
@Test fun nonNullConstructorParameterCalledWithNullFromAdapterFailsWithJsonDataException() {
val moshi = Moshi.Builder().add(object {
@FromJson fun fromJson(string: String): String? = null
}).build()
val jsonAdapter = moshi.adapter(HasNonNullConstructorParameter::class.java)
try {
jsonAdapter.fromJson("{\"a\":\"hello\"}")
fail()
} catch (expected: JsonDataException) {
assertThat(expected).hasMessage("Non-null value 'a' was null at \$.a")
}
}
@JsonClass(generateAdapter = true)
class HasNonNullConstructorParameter(val a: String)
@Test fun explicitNull() {
val moshi = Moshi.Builder().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)
}
@JsonClass(generateAdapter = true)
class ExplicitNull(var a: Int?, var b: Int?)
@Test fun absentNull() {
val moshi = Moshi.Builder().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)
}
@JsonClass(generateAdapter = true)
class AbsentNull(var a: Int?, var b: Int?)
@Test fun constructorParameterWithQualifier() {
val moshi = Moshi.Builder()
.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")
}
@JsonClass(generateAdapter = true)
class ConstructorParameterWithQualifier(@Uppercase(inFrench = true) var a: String, var b: String)
@Test fun propertyWithQualifier() {
val moshi = Moshi.Builder()
.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")
}
@JsonClass(generateAdapter = true)
class PropertyWithQualifier {
@Uppercase(inFrench = true) var a: String = ""
var b: String = ""
}
@Test fun constructorParameterWithJsonName() {
val moshi = Moshi.Builder().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)
}
@JsonClass(generateAdapter = true)
class ConstructorParameterWithJsonName(@Json(name = "key a") var a: Int, var b: Int)
@Test fun propertyWithJsonName() {
val moshi = Moshi.Builder().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)
}
@JsonClass(generateAdapter = true)
class PropertyWithJsonName {
@Json(name = "key a") var a: Int = -1
var b: Int = -1
}
@Test fun transientConstructorParameter() {
val moshi = Moshi.Builder().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)
}
@JsonClass(generateAdapter = true)
class TransientConstructorParameter(@Transient var a: Int = -1, var b: Int = -1)
@Test fun transientProperty() {
val moshi = Moshi.Builder().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)
}
@JsonClass(generateAdapter = true)
class TransientProperty {
@Transient var a: Int = -1
var b: Int = -1
}
@Test fun nonNullPropertySetToNullFailsWithJsonDataException() {
val moshi = Moshi.Builder().build()
val jsonAdapter = moshi.adapter(HasNonNullProperty::class.java)
try {
jsonAdapter.fromJson("{\"a\":null}")
fail()
} catch (expected: JsonDataException) {
assertThat(expected).hasMessage("Unexpected null at \$.a")
}
}
@Test fun nonNullPropertySetToNullFromAdapterFailsWithJsonDataException() {
val moshi = Moshi.Builder().add(object {
@FromJson fun fromJson(string: String): String? = null
}).build()
val jsonAdapter = moshi.adapter(HasNonNullProperty::class.java)
try {
jsonAdapter.fromJson("{\"a\":\"hello\"}")
fail()
} catch (expected: JsonDataException) {
assertThat(expected).hasMessage("Non-null value 'a' was null at \$.a")
}
}
@JsonClass(generateAdapter = true)
class HasNonNullProperty {
var a: String = ""
}
@Test fun manyProperties32() {
val moshi = Moshi.Builder().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)
}
@JsonClass(generateAdapter = true)
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)
@Test fun manyProperties33() {
val moshi = Moshi.Builder().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)
}
@JsonClass(generateAdapter = true)
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)
@Test fun unsettablePropertyIgnored() {
val moshi = Moshi.Builder().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)
}
@JsonClass(generateAdapter = true)
class UnsettableProperty {
val a: Int = -1
var b: Int = -1
}
@Test fun getterOnlyNoBackingField() {
val moshi = Moshi.Builder().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)
}
@JsonClass(generateAdapter = true)
class GetterOnly(var a: Int, var b: Int) {
val total : Int
get() = a + b
}
@Test fun getterAndSetterNoBackingField() {
val moshi = Moshi.Builder().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)
}
@JsonClass(generateAdapter = true)
class GetterAndSetter(var a: Int, var b: Int) {
var total : Int
get() = a + b
set(value) {
b = value - a
}
}
@Test fun supertypeConstructorParameters() {
val moshi = Moshi.Builder().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)
@JsonClass(generateAdapter = true)
class SubtypeConstructorParameters(a: Int, var b: Int) : SupertypeConstructorParameters(a)
@Test fun supertypeProperties() {
val moshi = Moshi.Builder().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
}
@JsonClass(generateAdapter = true)
class SubtypeProperties : SupertypeProperties() {
var b: Int = -1
}
/** Generated adapters don't track enough state to detect duplicated values. */
@Ignore @Test fun duplicatedValue() {
val moshi = Moshi.Builder().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")
}
}
@JsonClass(generateAdapter = true)
class DuplicateValue(var a: Int = -1, var b: Int = -2)
@JsonQualifier
annotation class Uppercase(val inFrench: Boolean, val onSundays: Boolean = false)
class UppercaseJsonAdapter {
@ToJson fun toJson(@Uppercase(inFrench = true) s: String) : String {
return s.toUpperCase(Locale.US)
}
@FromJson @Uppercase(inFrench = true) fun fromJson(s: String) : String {
return s.toLowerCase(Locale.US)
}
}
}
// Has to be outside to avoid Types seeing an owning class
@JsonClass(generateAdapter = true)
data class NullableTypeParams<T>(
val nullableList: List<String?>,
val nullableSet: Set<String?>,
val nullableMap: Map<String, String?>,
val nullableT: T?,
val nonNullT: 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.
*/
@JsonClass(generateAdapter = true)
data class SmokeTestType(
@Json(name = "first_name") val firstName: String,
@Json(name = "last_name") val lastName: String,
val age: Int,
val nationalities: List<String> = 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<out String> = emptyList(),
val wildcardIn: Array<in String>,
val any: List<*>,
val anyTwo: List<Any>,
val anyOut: List<out Any>,
val favoriteThreeNumbers: IntArray,
val favoriteArrayValues: Array<String>,
val favoriteNullableArrayValues: Array<String?>,
val nullableSetListMapArrayNullableIntWithDefault: Set<List<Map<String, Array<IntArray?>>>>? = null,
val aliasedName: TypeAliasName = "Woah"
)