mirror of
https://github.com/fankes/moshi.git
synced 2025-10-19 16:09:21 +08:00
Move the Kotlin code generator to the kotlin/ module
This commit is contained in:
156
kotlin/codegen/pom.xml
Normal file
156
kotlin/codegen/pom.xml
Normal 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>
|
@@ -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()
|
||||
}
|
||||
}
|
@@ -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)
|
||||
}
|
||||
}
|
||||
}
|
@@ -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
|
||||
}
|
||||
}
|
@@ -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() }
|
||||
}
|
||||
}
|
@@ -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()
|
||||
}
|
||||
}
|
@@ -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)
|
||||
}
|
||||
}
|
||||
}
|
@@ -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
|
||||
)
|
@@ -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
|
||||
}
|
@@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@@ -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
|
||||
}
|
||||
}
|
||||
}
|
@@ -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")
|
||||
}
|
||||
}
|
||||
}
|
@@ -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
|
||||
}
|
@@ -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)
|
||||
}
|
@@ -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;
|
||||
}
|
@@ -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")
|
||||
}
|
||||
}
|
@@ -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()
|
||||
}
|
||||
}
|
@@ -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
|
||||
)
|
@@ -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
143
kotlin/tests/pom.xml
Normal 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>
|
@@ -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"
|
||||
)
|
Reference in New Issue
Block a user