Kotlin Code Gen module (#435)

* Add kotlin code gen modules

* Update kotlin to 1.2

* Add a serializable dummy class

* Try using kapt configuration from kotlin-examples repo

Still no luck!

* Use proper allocated name for assignment too

* Use selectName() API

* Clean up constructor parameter annotations & plumbing for qualifiers

* Updates poms and kotlin code gen processor to support tests.

* Ignore kotlin code gen tests for now

None of these are data classes tests right now, which is the only thing this supports right now

* Replace $ with _ in class names for consistency

* Shortcut Array types to arrayOf

* Add DataClassTest

* Try generated option first, fall back to maven after

* More idiomatic handling

* Only use nonnullable types for adapter properties

* Code dump of kotshi tests

* Comment out specifics to get compiling

* Generics support!

* Fix double primitive default

* Pick up temporary snapshot for Any fix

* Invariance should just be null

* Better handling of nullably-bound variance

* Just assume the first jvm constructor for now as jvmMethodSig is flaky

* Specify types param if needed

* Don't do lazy delegation

* Clean up nullable typevariablename boundaries

* Add type variables to extension function on companion object

* Use properties instead of allocated names for more robustness

Since we're already on a snapshot

* If there are no type variables, make it null for simpler handling

* Fix generics and Type[] handling

* Fix unnecessary as casts on primitive defaults

* Reference spec directly for possible bangs

* Use nullSafe() adapters for anything nullable or with default values

* Use object type in makeType()

Types.java cares

* Make TestPrimitiveDefaultValues work

* Re-enable TestClassWithJavaKeyword

* Ignore remaining tests that are pending decisions or JsonQualifier support

* Remove customnames test as we're just going to stick with simple @Json

* Add toString() implementations

* Reenable default values testing, adapt to kotlin lang support

* Remove primitive adapters bits since we're not using it

* Clean up a bunch of leftover comments

* Switch to only nullable handling, report missing properties

This makes all nullable handling for local properties the same, and removes defaults for primitives in the process. It simplifies the handling a lot, and leans on kotlin language features to take care of null handling (null checking and then throwing the lazily evaluated list of missing properties).

One minor change from what kotshi does - this reports the serialized name in the missing properties, not the property name. We could look at supporting this though if we want.

* Implement JsonQualifier support

* Use Kapt for AutoService/processor declaration

* Checkstyle

* Remove unused primite type checks

* Add test verifying mutable and immutable collections work

* Fix test name

* Standardize isRequired checks

* Add more nullability and mutability tests

* Kotlinpoet 0.7.0 final

* Switch to new vararg overload for annotation class adapter()

* Make suffix just JsonAdapter without underscore

* Switch to just a regular constructor for MoshiSerializableFactory

* Remove constructor caching

* Remove unnecessary framework class checks

* Nix unnecessary superclass lookups, inline constructor lookup

* Nix null token check in reads

* Nix null check in writes, do !! on first value use

* Nix null checks in favor of serializeNulls

* Inline null checks and fail eagerly

* Fix double _Adapter

* First pass at simplifying adapter names

* Inline names to options property, life into class and rm companion

* Differentiate between absent and null, use nullSafe() as needed

* Group together compile and test dependencies

* Remove incorrect comment

* Revert formatting

* Set, not mutable set

* Collapse else-if nesting to one when

* Cleaner formatting test code

* Collapse more to locals

* Collapse more

* Return a nonnullable type in fromJson

* Remove redundant out variance

* Use KClass where appropriate

* End comment in period

* Remove redundant comment

* Throw on unrecognized type in simplified name

* Use illegalargumentexception instead

* Emit a nullcheck at the beginning of toJson instead

* Remove extra newline

* Simplify processing to be less abusive

* Skip using asClassName() when possible

* Use addComment()

* Switch to declared constructors

Technically more correct since we're defining these

* Unmodifiable set

* return adapter(type, annotationTypes[0])

* Slight optimization - check if the type is parameterized first

If the type is a parameterized type, then we know they'll have the two-arg constructor. This way we don't always try and fail the single arg constructor on parameterized types

* Add test for type aliases, optimize to reuse adapters if possible

This is a tiny optimization to make type aliases (which did already work) reuse adapter properties if they already exist for the backing type. What this means is that if you have:

typealias Foo = String

and properties
foo: Foo
bar: String

you'll only get one adapter property field for String, and both will use it

* Use string templating where possible

* Remove all the kotshi tests
This commit is contained in:
Zac Sweers
2018-03-11 18:17:55 -07:00
committed by Jesse Wilson
parent e6c2ebedde
commit 96e074d030
10 changed files with 2268 additions and 1 deletions

View File

@@ -0,0 +1,134 @@
<?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-compiler</artifactId>
<repositories>
<repository>
<id>jcenter</id>
<url>https://jcenter.bintray.com/</url>
</repository>
</repositories>
<dependencies>
<dependency>
<groupId>com.squareup.moshi</groupId>
<artifactId>moshi</artifactId>
<version>${project.version}</version>
</dependency>
<dependency>
<groupId>com.squareup.moshi</groupId>
<artifactId>moshi-kotlin-codegen-runtime</artifactId>
<version>${project.version}</version>
</dependency>
<dependency>
<groupId>org.jetbrains.kotlin</groupId>
<artifactId>kotlin-stdlib</artifactId>
</dependency>
<dependency>
<groupId>me.eugeniomarletti</groupId>
<artifactId>kotlin-metadata</artifactId>
<version>1.2.1</version>
</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>
</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>
</plugins>
</build>
</project>

View File

@@ -0,0 +1,764 @@
/*
* Copyright (C) 2018 Square, Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.squareup.moshi
import com.google.auto.common.AnnotationMirrors
import com.google.auto.service.AutoService
import com.squareup.kotlinpoet.ANY
import com.squareup.kotlinpoet.ARRAY
import com.squareup.kotlinpoet.BOOLEAN
import com.squareup.kotlinpoet.BYTE
import com.squareup.kotlinpoet.CHAR
import com.squareup.kotlinpoet.ClassName
import com.squareup.kotlinpoet.CodeBlock
import com.squareup.kotlinpoet.DOUBLE
import com.squareup.kotlinpoet.FLOAT
import com.squareup.kotlinpoet.FileSpec
import com.squareup.kotlinpoet.FunSpec
import com.squareup.kotlinpoet.INT
import com.squareup.kotlinpoet.KModifier
import com.squareup.kotlinpoet.KModifier.IN
import com.squareup.kotlinpoet.KModifier.OUT
import com.squareup.kotlinpoet.KModifier.OVERRIDE
import com.squareup.kotlinpoet.KModifier.PRIVATE
import com.squareup.kotlinpoet.LONG
import com.squareup.kotlinpoet.NameAllocator
import com.squareup.kotlinpoet.ParameterSpec
import com.squareup.kotlinpoet.ParameterizedTypeName
import com.squareup.kotlinpoet.PropertySpec
import com.squareup.kotlinpoet.SHORT
import com.squareup.kotlinpoet.TypeName
import com.squareup.kotlinpoet.TypeSpec
import com.squareup.kotlinpoet.TypeVariableName
import com.squareup.kotlinpoet.WildcardTypeName
import com.squareup.kotlinpoet.asClassName
import com.squareup.kotlinpoet.asTypeName
import me.eugeniomarletti.kotlin.metadata.KotlinClassMetadata
import me.eugeniomarletti.kotlin.metadata.KotlinMetadataUtils
import me.eugeniomarletti.kotlin.metadata.declaresDefaultValue
import me.eugeniomarletti.kotlin.metadata.extractFullName
import me.eugeniomarletti.kotlin.metadata.isDataClass
import me.eugeniomarletti.kotlin.metadata.isPrimary
import me.eugeniomarletti.kotlin.metadata.jvm.getJvmConstructorSignature
import me.eugeniomarletti.kotlin.metadata.kotlinMetadata
import me.eugeniomarletti.kotlin.metadata.visibility
import me.eugeniomarletti.kotlin.processing.KotlinAbstractProcessor
import org.jetbrains.kotlin.serialization.ProtoBuf
import org.jetbrains.kotlin.serialization.ProtoBuf.Type.Argument.Projection
import org.jetbrains.kotlin.serialization.ProtoBuf.TypeParameter.Variance
import org.jetbrains.kotlin.serialization.ProtoBuf.Visibility
import org.jetbrains.kotlin.serialization.ProtoBuf.Visibility.INTERNAL
import org.jetbrains.kotlin.serialization.deserialization.NameResolver
import java.io.File
import java.lang.reflect.Type
import javax.annotation.processing.Processor
import javax.annotation.processing.RoundEnvironment
import javax.lang.model.SourceVersion
import javax.lang.model.element.AnnotationMirror
import javax.lang.model.element.Element
import javax.lang.model.element.ElementKind
import javax.lang.model.element.ExecutableElement
import javax.lang.model.element.TypeElement
import javax.lang.model.util.Elements
import javax.tools.Diagnostic.Kind.ERROR
/**
* An annotation processor that reads Kotlin data classes and generates Moshi JsonAdapters for them.
* This generates Kotlin code, and understands basic Kotlin language features like default values
* and companion objects.
*
* The generated class will match the visibility of the given data class (i.e. if it's internal, the
* adapter will also be internal).
*
* If you define a companion object, a jsonAdapter() extension function will be generated onto it.
* If you don't want this though, you can use the runtime [MoshiSerializable] factory implementation.
*/
@AutoService(Processor::class)
class MoshiKotlinCodeGenProcessor : KotlinAbstractProcessor(), KotlinMetadataUtils {
private val annotationName = MoshiSerializable::class.java.canonicalName
override fun getSupportedAnnotationTypes() = setOf(annotationName)
override fun getSupportedSourceVersion(): SourceVersion = SourceVersion.latest()
override fun process(annotations: Set<TypeElement>, roundEnv: RoundEnvironment): Boolean {
val annotationElement = elementUtils.getTypeElement(annotationName)
roundEnv.getElementsAnnotatedWith(annotationElement)
.asSequence()
.mapNotNull { processElement(it) }
.forEach { it.generateAndWrite() }
return true
}
private fun processElement(element: Element): Adapter? {
val metadata = element.kotlinMetadata
if (metadata !is KotlinClassMetadata) {
errorMustBeDataClass(element)
return null
}
val classData = metadata.data
val (nameResolver, classProto) = classData
fun ProtoBuf.Type.extractFullName() = extractFullName(classData)
if (!classProto.isDataClass) {
errorMustBeDataClass(element)
return null
}
val fqClassName = nameResolver.getString(classProto.fqName).replace('/', '.')
val packageName = nameResolver.getString(classProto.fqName).substringBeforeLast('/').replace(
'/', '.')
val hasCompanionObject = classProto.hasCompanionObjectName()
// todo allow custom constructor
val protoConstructor = classProto.constructorList
.single { it.isPrimary }
val constructorJvmSignature = protoConstructor.getJvmConstructorSignature(nameResolver,
classProto.typeTable)
val constructor = classProto.fqName
.let(nameResolver::getString)
.replace('/', '.')
.let(elementUtils::getTypeElement)
.enclosedElements
.mapNotNull { it.takeIf { it.kind == ElementKind.CONSTRUCTOR }?.let { it as ExecutableElement } }
.first()
// TODO Temporary until jvm method signature matching is better
// .single { it.jvmMethodSignature == constructorJvmSignature }
val parameters = protoConstructor
.valueParameterList
.mapIndexed { index, valueParameter ->
val paramName = nameResolver.getString(valueParameter.name)
val nullable = valueParameter.type.nullable
val paramFqcn = valueParameter.type.extractFullName()
.replace("`", "")
.removeSuffix("?")
val actualElement = constructor.parameters[index]
val serializedName = actualElement.getAnnotation(Json::class.java)?.name
?: paramName
val jsonQualifiers = AnnotationMirrors.getAnnotatedAnnotations(actualElement,
JsonQualifier::class.java)
Property(
name = paramName,
fqClassName = paramFqcn,
serializedName = serializedName,
hasDefault = valueParameter.declaresDefaultValue,
nullable = nullable,
typeName = valueParameter.type.asTypeName(nameResolver, classProto::getTypeParameter),
unaliasedName = valueParameter.type.asTypeName(nameResolver, classProto::getTypeParameter, true),
jsonQualifiers = jsonQualifiers)
}
val genericTypeNames = classProto.typeParameterList
.map {
val variance = it.variance.asKModifier().let {
// We don't redeclare out variance here
if (it == OUT) {
null
} else {
it
}
}
TypeVariableName.invoke(
name = nameResolver.getString(it.name),
bounds = *(it.upperBoundList
.map { it.asTypeName(nameResolver, classProto::getTypeParameter) }
.toTypedArray()),
variance = variance)
.reified(it.reified)
}.let {
if (it.isEmpty()) {
null
} else {
it
}
}
return Adapter(
fqClassName = fqClassName,
packageName = packageName,
propertyList = parameters,
originalElement = element,
hasCompanionObject = hasCompanionObject,
visibility = classProto.visibility!!,
genericTypeNames = genericTypeNames,
elementUtils = elementUtils)
}
private fun errorMustBeDataClass(element: Element) {
messager.printMessage(ERROR,
"@${MoshiSerializable::class.java.simpleName} can't be applied to $element: must be a Kotlin data class",
element)
}
private fun Adapter.generateAndWrite() {
val adapterName = "${name}JsonAdapter"
val outputDir = generatedDir ?: mavenGeneratedDir(adapterName)
val fileBuilder = FileSpec.builder(packageName, adapterName)
generate(adapterName, fileBuilder)
fileBuilder
.build()
.writeTo(outputDir)
}
private fun mavenGeneratedDir(adapterName: String): File {
// Hack since the maven plugin doesn't supply `kapt.kotlin.generated` option
// Bug filed at https://youtrack.jetbrains.com/issue/KT-22783
val file = filer.createSourceFile(adapterName).toUri().let(::File)
return file.parentFile.also { file.delete() }
}
}
/**
* Creates a joined string representation of simplified typename names.
*/
private fun List<TypeName>.simplifiedNames(): String {
return joinToString("_") { it.simplifiedName() }
}
private fun TypeName.resolveRawType(): ClassName {
return when (this) {
is ClassName -> this
is ParameterizedTypeName -> rawType
else -> throw IllegalArgumentException("Cannot get raw type from $this")
}
}
/**
* Creates a simplified string representation of a TypeName's name
*/
private fun TypeName.simplifiedName(): String {
return when (this) {
is ClassName -> simpleName().decapitalize()
is ParameterizedTypeName -> {
rawType.simpleName().decapitalize() + if (typeArguments.isEmpty()) "" else "__" + typeArguments.simplifiedNames()
}
is WildcardTypeName -> "wildcard__" + (lowerBounds + upperBounds).simplifiedNames()
is TypeVariableName -> name.decapitalize() + if (bounds.isEmpty()) "" else "__" + bounds.simplifiedNames()
else -> throw IllegalArgumentException("Unrecognized type! $this")
}.let { if (nullable) "${it}_nullable" else it }
}
private fun ClassName.isClass(elementUtils: Elements): Boolean {
val fqcn = toString()
if (fqcn.startsWith("kotlin.collections.")) {
// These are special kotlin interfaces are only visible in kotlin, because they're replaced by
// the compiler with concrete java classes
return false
} else if (this == ARRAY) {
// This is a "fake" class and not visible to Elements
return true
}
return elementUtils.getTypeElement(fqcn).kind == ElementKind.INTERFACE
}
private fun TypeName.objectType(): TypeName {
return when (this) {
BOOLEAN -> Boolean::class.javaObjectType.asTypeName()
BYTE -> Byte::class.javaObjectType.asTypeName()
SHORT -> Short::class.javaObjectType.asTypeName()
INT -> Integer::class.javaObjectType.asTypeName()
LONG -> Long::class.javaObjectType.asTypeName()
CHAR -> Character::class.javaObjectType.asTypeName()
FLOAT -> Float::class.javaObjectType.asTypeName()
DOUBLE -> Double::class.javaObjectType.asTypeName()
else -> this
}
}
private fun TypeName.makeType(
elementUtils: Elements,
typesArray: ParameterSpec,
genericTypeNames: List<TypeVariableName>): CodeBlock {
if (nullable) {
return asNonNullable().makeType(elementUtils, typesArray, genericTypeNames)
}
return when (this) {
is ClassName -> CodeBlock.of("%T::class.java", this)
is ParameterizedTypeName -> {
// If it's an Array type, we shortcut this to return Types.arrayOf()
if (rawType == ARRAY) {
return CodeBlock.of("%T.arrayOf(%L)",
Types::class,
typeArguments[0].objectType().makeType(elementUtils, typesArray, genericTypeNames))
}
// If it's a Class type, we have to specify the generics.
val rawTypeParameters = if (rawType.isClass(elementUtils)) {
CodeBlock.of(
typeArguments.joinTo(
buffer = StringBuilder(),
separator = ", ",
prefix = "<",
postfix = ">") { "%T" }
.toString(),
*(typeArguments.map { objectType() }.toTypedArray())
)
} else {
CodeBlock.of("")
}
CodeBlock.of(
"%T.newParameterizedType(%T%L::class.java, ${typeArguments
.joinToString(", ") { "%L" }})",
Types::class,
rawType.objectType(),
rawTypeParameters,
*(typeArguments.map {
it.objectType().makeType(elementUtils, typesArray, genericTypeNames)
}.toTypedArray()))
}
is WildcardTypeName -> {
val target: TypeName
val method: String
when {
lowerBounds.size == 1 -> {
target = lowerBounds[0]
method = "supertypeOf"
}
upperBounds.size == 1 -> {
target = upperBounds[0]
method = "subtypeOf"
}
else -> throw IllegalArgumentException(
"Unrepresentable wildcard type. Cannot have more than one bound: " + this)
}
CodeBlock.of("%T.%L(%T::class.java)", Types::class, method, target)
}
is TypeVariableName -> {
CodeBlock.of("%N[%L]", typesArray, genericTypeNames.indexOfFirst { it == this })
}
else -> throw IllegalArgumentException("Unrepresentable type: " + this)
}
}
private data class Property(
val name: String,
val fqClassName: String,
val serializedName: String,
val hasDefault: Boolean,
val nullable: Boolean,
val typeName: TypeName,
val unaliasedName: TypeName,
val jsonQualifiers: Set<AnnotationMirror>) {
val isRequired = !nullable && !hasDefault
}
private data class Adapter(
val fqClassName: String,
val packageName: String,
val propertyList: List<Property>,
val originalElement: Element,
val name: String = fqClassName.substringAfter(packageName)
.replace('.', '_')
.removePrefix("_"),
val hasCompanionObject: Boolean,
val visibility: Visibility,
val elementUtils: Elements,
val genericTypeNames: List<TypeVariableName>?) {
fun generate(adapterName: String, fileSpecBuilder: FileSpec.Builder) {
val nameAllocator = NameAllocator()
fun String.allocate() = nameAllocator.newName(this)
val originalTypeName = originalElement.asType().asTypeName()
val moshiName = "moshi".allocate()
val moshiParam = ParameterSpec.builder(moshiName, Moshi::class).build()
val typesParam = ParameterSpec.builder("types".allocate(),
ParameterizedTypeName.get(ARRAY, Type::class.asTypeName())).build()
val reader = ParameterSpec.builder("reader".allocate(),
JsonReader::class).build()
val writer = ParameterSpec.builder("writer".allocate(),
JsonWriter::class).build()
val value = ParameterSpec.builder("value".allocate(),
originalTypeName.asNullable()).build()
val jsonAdapterTypeName = ParameterizedTypeName.get(JsonAdapter::class.asClassName(),
originalTypeName)
// Create fields
val adapterProperties = propertyList
.distinctBy { it.unaliasedName to it.jsonQualifiers }
.associate { prop ->
val typeName = prop.unaliasedName
val qualifierNames = prop.jsonQualifiers.joinToString("") {
"at${it.annotationType.asElement().simpleName.toString().capitalize()}"
}
val propertyName = typeName.simplifiedName().allocate().let {
if (qualifierNames.isBlank()) {
it
} else {
"$it$qualifierNames"
}
}.let { "${it}Adapter" }
val adapterTypeName = ParameterizedTypeName.get(JsonAdapter::class.asTypeName(), typeName)
val key = typeName to prop.jsonQualifiers
return@associate key to PropertySpec.builder(propertyName, adapterTypeName, PRIVATE)
.apply {
val qualifiers = prop.jsonQualifiers.toList()
val standardArgs = arrayOf(moshiParam,
if (typeName is ClassName && qualifiers.isEmpty()) {
""
} else {
CodeBlock.of("<%T>",
typeName)
},
typeName.makeType(elementUtils, typesParam, genericTypeNames ?: emptyList()))
val standardArgsSize = standardArgs.size + 1
val (initializerString, args) = when {
qualifiers.isEmpty() -> "" to emptyArray()
qualifiers.size == 1 -> {
", %${standardArgsSize}T::class.java" to arrayOf(
qualifiers.first().annotationType.asTypeName())
}
else -> {
val initString = qualifiers
.mapIndexed { index, _ ->
val annoClassIndex = standardArgsSize + index
return@mapIndexed "%${annoClassIndex}T::class.java"
}
.joinToString()
val initArgs = qualifiers
.map { it.annotationType.asTypeName() }
.toTypedArray()
", $initString" to initArgs
}
}
val finalArgs = arrayOf(*standardArgs, *args)
initializer(
"%1N.adapter%2L(%3L$initializerString)${if (prop.nullable) ".nullSafe()" else ""}",
*finalArgs)
}
.build()
}
val localProperties =
propertyList.associate { prop ->
val propertySpec = PropertySpec.builder(prop.name.allocate(), prop.typeName.asNullable())
.mutable(true)
.initializer("null")
.build()
val propertySetSpec = if (prop.hasDefault && prop.nullable) {
PropertySpec.builder("${propertySpec.name}Set".allocate(), BOOLEAN)
.mutable(true)
.initializer("false")
.build()
} else {
null
}
val specs = propertySpec to propertySetSpec
prop to specs
}
val optionsByIndex = propertyList
.associateBy { it.serializedName }.entries.withIndex()
// selectName() API setup
val optionsCN = JsonReader.Options::class.asTypeName()
val optionsProperty = PropertySpec.builder(
"options".allocate(),
optionsCN,
PRIVATE)
.initializer("%T.of(${optionsByIndex.map { it.value.key }
.joinToString(", ") { "\"$it\"" }})",
optionsCN)
.build()
val adapter = TypeSpec.classBuilder(adapterName)
.superclass(jsonAdapterTypeName)
.apply {
genericTypeNames?.let {
addTypeVariables(genericTypeNames)
}
}
.apply {
// TODO make this configurable. Right now it just matches the source model
if (visibility == INTERNAL) {
addModifiers(KModifier.INTERNAL)
}
}
.primaryConstructor(FunSpec.constructorBuilder()
.addParameter(moshiParam)
.apply {
genericTypeNames?.let {
addParameter(typesParam)
}
}
.build())
.addProperty(optionsProperty)
.addProperties(adapterProperties.values)
.addFunction(FunSpec.builder("toString")
.addModifiers(OVERRIDE)
.returns(String::class)
.addStatement("return %S",
"GeneratedJsonAdapter(${originalTypeName.resolveRawType()
.simpleNames()
.joinToString(".")})")
.build())
.addFunction(FunSpec.builder("fromJson")
.addModifiers(OVERRIDE)
.addParameter(reader)
.returns(originalTypeName)
.apply {
localProperties.values.forEach {
addCode("%L", it.first)
it.second?.let {
addCode("%L", it)
}
}
}
.addStatement("%N.beginObject()", reader)
.beginControlFlow("while (%N.hasNext())", reader)
.beginControlFlow("when (%N.selectName(%N))", reader, optionsProperty)
.apply {
optionsByIndex.map { (index, entry) -> index to entry.value }
.forEach { (index, prop) ->
val specs = localProperties[prop]!!
val spec = specs.first
val setterSpec = specs.second
if (setterSpec != null) {
beginControlFlow("%L -> ", index)
addStatement("%N = %N.fromJson(%N)",
spec,
adapterProperties[prop.unaliasedName to prop.jsonQualifiers]!!,
reader)
addStatement("%N = true", setterSpec)
endControlFlow()
} else {
addStatement("%L -> %N = %N.fromJson(%N)",
index,
spec,
adapterProperties[prop.unaliasedName to prop.jsonQualifiers]!!,
reader)
}
}
}
.beginControlFlow("-1 ->")
.addComment("Unknown name, skip it.")
.addStatement("%N.nextName()", reader)
.addStatement("%N.skipValue()", reader)
.endControlFlow()
.endControlFlow()
.endControlFlow()
.addStatement("%N.endObject()", reader)
.apply {
val propertiesWithDefaults = localProperties.entries.filter { it.key.hasDefault }
val propertiesWithoutDefaults = localProperties.entries.filter { !it.key.hasDefault }
val requiredPropertiesCodeBlock = CodeBlock.of(
propertiesWithoutDefaults.joinToString(",\n") { (property, specs) ->
val spec = specs.first
"${property.name} = ${spec.name}%L"
},
*(propertiesWithoutDefaults
.map { (property, _) ->
if (property.isRequired) {
@Suppress("IMPLICIT_CAST_TO_ANY")
CodeBlock.of(
" ?: throw %T(\"Required property '%L' missing at \${%N.path}\")",
JsonDataException::class,
property.name,
reader
)
} else {
@Suppress("IMPLICIT_CAST_TO_ANY")
""
}
}
.toTypedArray()))
if (propertiesWithDefaults.isEmpty()) {
addStatement("return %T(%L)",
originalTypeName,
requiredPropertiesCodeBlock)
} else {
addStatement("return %T(%L)\n.let {\n it.copy(%L)\n}",
originalTypeName,
requiredPropertiesCodeBlock,
propertiesWithDefaults
.joinToString(",\n ") { (property, specs) ->
val spec = specs.first
val setSpec = specs.second
if (setSpec != null) {
"${property.name} = if (${setSpec.name}) ${spec.name} else it.${property.name}"
} else {
"${property.name} = ${spec.name} ?: it.${property.name}"
}
})
}
}
.build())
.addFunction(FunSpec.builder("toJson")
.addModifiers(OVERRIDE)
.addParameter(writer)
.addParameter(value)
.beginControlFlow("if (%N == null)", value)
.addStatement("throw %T(%S)", NullPointerException::class, "${value.name} was null! Wrap in .nullSafe() to write nullable values.")
.endControlFlow()
.addStatement("%N.beginObject()", writer)
.apply {
propertyList.forEach { prop ->
addStatement("%N.name(%S)",
writer,
prop.serializedName)
addStatement("%N.toJson(%N, %N.%L)",
adapterProperties[prop.unaliasedName to prop.jsonQualifiers]!!,
writer,
value,
prop.name)
}
}
.addStatement("%N.endObject()", writer)
.build())
.build()
if (hasCompanionObject) {
val rawType = when (originalTypeName) {
is TypeVariableName -> throw IllegalArgumentException(
"Cannot get raw type of TypeVariable!")
is ParameterizedTypeName -> originalTypeName.rawType
else -> originalTypeName as ClassName
}
fileSpecBuilder.addFunction(FunSpec.builder("jsonAdapter")
.apply {
// TODO make this configurable. Right now it just matches the source model
if (visibility == INTERNAL) {
addModifiers(KModifier.INTERNAL)
}
}
.receiver(rawType.nestedClass("Companion"))
.returns(jsonAdapterTypeName)
.addParameter(moshiParam)
.apply {
genericTypeNames?.let {
addParameter(typesParam)
addTypeVariables(it)
}
}
.apply {
if (genericTypeNames != null) {
addStatement("return %N(%N, %N)", adapter, moshiParam, typesParam)
} else {
addStatement("return %N(%N)", adapter, moshiParam)
}
}
.build())
}
fileSpecBuilder.addType(adapter)
}
}
private fun ProtoBuf.TypeParameter.asTypeName(
nameResolver: NameResolver,
getTypeParameter: (index: Int) -> ProtoBuf.TypeParameter,
resolveAliases: Boolean = false): TypeName {
return TypeVariableName(
name = nameResolver.getString(name),
bounds = *(upperBoundList.map { it.asTypeName(nameResolver, getTypeParameter, resolveAliases) }
.toTypedArray()),
variance = variance.asKModifier()
)
}
private fun ProtoBuf.TypeParameter.Variance.asKModifier(): KModifier? {
return when (this) {
Variance.IN -> IN
Variance.OUT -> OUT
Variance.INV -> null
}
}
/**
* Returns the TypeName of this type as it would be seen in the source code,
* including nullability and generic type parameters.
*
* @param [nameResolver] a [NameResolver] instance from the source proto
* @param [getTypeParameter]
* A function that returns the type parameter for the given index.
* **Only called if [ProtoBuf.Type.hasTypeParameter] is `true`!**
*/
private fun ProtoBuf.Type.asTypeName(
nameResolver: NameResolver,
getTypeParameter: (index: Int) -> ProtoBuf.TypeParameter,
resolveAliases: Boolean = false
): TypeName {
val argumentList = when {
hasAbbreviatedType() -> abbreviatedType.argumentList
else -> argumentList
}
if (hasFlexibleUpperBound()) {
return WildcardTypeName.subtypeOf(
flexibleUpperBound.asTypeName(nameResolver, getTypeParameter, resolveAliases))
} else if (hasOuterType()) {
return WildcardTypeName.supertypeOf(outerType.asTypeName(nameResolver, getTypeParameter, resolveAliases))
}
val realType = when {
hasTypeParameter() -> return getTypeParameter(typeParameter)
.asTypeName(nameResolver, getTypeParameter, resolveAliases)
hasTypeParameterName() -> typeParameterName
hasAbbreviatedType() && !resolveAliases -> abbreviatedType.typeAliasName
else -> className
}
var typeName: TypeName = ClassName.bestGuess(nameResolver.getString(realType)
.replace("/", "."))
if (argumentList.isNotEmpty()) {
val remappedArgs: Array<TypeName> = argumentList.map {
val projection = if (it.hasProjection()) {
it.projection
} else null
if (it.hasType()) {
it.type.asTypeName(nameResolver, getTypeParameter, resolveAliases)
.let { typeName ->
projection?.let {
when (it) {
Projection.IN -> WildcardTypeName.supertypeOf(typeName)
Projection.OUT -> {
if (typeName == ANY) {
// This becomes a *, which we actually don't want here.
// List<Any> works with List<*>, but List<*> doesn't work with List<Any>
typeName
} else {
WildcardTypeName.subtypeOf(typeName)
}
}
Projection.STAR -> WildcardTypeName.subtypeOf(ANY)
Projection.INV -> TODO("INV projection is unsupported")
}
} ?: typeName
}
} else {
WildcardTypeName.subtypeOf(ANY)
}
}.toTypedArray()
typeName = ParameterizedTypeName.get(typeName as ClassName, *remappedArgs)
}
if (nullable) {
typeName = typeName.asNullable()
}
return typeName
}

View File

@@ -0,0 +1,153 @@
<?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-integration</artifactId>
<dependencies>
<dependency>
<groupId>com.squareup.moshi</groupId>
<artifactId>moshi</artifactId>
<version>${project.version}</version>
</dependency>
<dependency>
<groupId>com.squareup.moshi</groupId>
<artifactId>moshi-kotlin-codegen-runtime</artifactId>
<version>${project.version}</version>
</dependency>
<dependency>
<groupId>org.jetbrains.kotlin</groupId>
<artifactId>kotlin-stdlib</artifactId>
</dependency>
<dependency>
<groupId>org.jetbrains.kotlin</groupId>
<artifactId>kotlin-reflect</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>
<sourceDir>src/main/java</sourceDir>
</sourceDirs>
<annotationProcessorPaths>
<annotationProcessorPath>
<groupId>com.squareup.moshi</groupId>
<artifactId>moshi-kotlin-codegen-compiler</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-compiler</artifactId>
<version>${project.version}</version>
</annotationProcessorPath>
</annotationProcessorPaths>
</configuration>
</execution>
<execution>
<id>test-compile</id>
<goals>
<goal>test-compile</goal>
</goals>
<configuration>
<sourceDirs>
<sourceDir>src/test/kotlin</sourceDir>
<sourceDir>src/test/java</sourceDir>
<sourceDir>target/generated-sources/kapt/test</sourceDir>
</sourceDirs>
</configuration>
</execution>
</executions>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<version>3.5.1</version>
<configuration>
<proc>none</proc>
<source>1.6</source>
<target>1.6</target>
</configuration>
<executions>
<!-- Replacing default-compile as it is treated specially by maven -->
<execution>
<id>default-compile</id>
<phase>none</phase>
</execution>
<!-- Replacing default-testCompile as it is treated specially by maven -->
<execution>
<id>default-testCompile</id>
<phase>none</phase>
</execution>
<execution>
<id>java-compile</id>
<phase>compile</phase>
<goals>
<goal>compile</goal>
</goals>
</execution>
<execution>
<id>java-test-compile</id>
<phase>test-compile</phase>
<goals> <goal>testCompile</goal> </goals>
</execution>
</executions>
</plugin>
</plugins>
</build>
</project>

View File

@@ -0,0 +1,280 @@
/*
* Copyright (C) 2018 Square, Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.squareup.moshi
import org.assertj.core.api.Assertions.assertThat
import org.assertj.core.api.Assertions.fail
import org.intellij.lang.annotations.Language
import org.junit.Test
class DataClassesTest {
private val moshi = Moshi.Builder().add(MoshiSerializableFactory()).build()
@Test
fun jsonAnnotation() {
val adapter = moshi.adapter(JsonAnnotation::class.java)
// Read
@Language("JSON")
val json = """{"foo": "bar"}"""
val instance = adapter.fromJson(json)!!
assertThat(instance.bar).isEqualTo("bar")
// Write
@Language("JSON")
val expectedJson = """{"foo":"baz"}"""
assertThat(adapter.toJson(JsonAnnotation("baz"))).isEqualTo(expectedJson)
}
@MoshiSerializable
data class JsonAnnotation(@Json(name = "foo") val bar: String)
@Test
fun defaultValues() {
val adapter = moshi.adapter(DefaultValues::class.java)
// Read/write with default values
@Language("JSON")
val json = """{"foo":"fooString"}"""
val instance = adapter.fromJson(json)!!
assertThat(instance.foo).isEqualTo("fooString")
assertThat(instance.bar).isEqualTo("")
assertThat(instance.nullableBar).isNull()
assertThat(instance.bazList).apply {
isNotNull()
isEmpty()
}
@Language("JSON") val expected = """{"foo":"fooString","bar":"","bazList":[]}"""
assertThat(adapter.toJson(DefaultValues("fooString"))).isEqualTo(expected)
// Read/write with real values
@Language("JSON")
val json2 = """
{"foo":"fooString","bar":"barString","nullableBar":"bar","bazList":["baz"]}
""".trimIndent()
val instance2 = adapter.fromJson(json2)!!
assertThat(instance2.foo).isEqualTo("fooString")
assertThat(instance2.bar).isEqualTo("barString")
assertThat(instance2.nullableBar).isEqualTo("bar")
assertThat(instance2.bazList).containsExactly("baz")
assertThat(adapter.toJson(instance2)).isEqualTo(json2)
}
@MoshiSerializable
data class DefaultValues(val foo: String,
val bar: String = "",
val nullableBar: String? = null,
val bazList: List<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)
}
@MoshiSerializable
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)
}
@MoshiSerializable
data class PrimitiveArray(val ints: IntArray)
@Test
fun nullableTypes() {
val adapter = moshi.adapter(NullabeTypes::class.java)
@Language("JSON")
val json = """{"foo":"foo","nullableString":null}"""
@Language("JSON")
val invalidJson = """{"foo":null,"nullableString":null}"""
val instance = adapter.fromJson(json)!!
assertThat(instance.foo).isEqualTo("foo")
assertThat(instance.nullableString).isNull()
try {
adapter.fromJson(invalidJson)
fail("The invalid json should have failed!")
} catch (e: JsonDataException) {
assertThat(e).hasMessageContaining("foo")
}
}
@MoshiSerializable
data class NullabeTypes(
val foo: String,
val nullableString: String?
)
@Test
fun collections() {
val adapter = moshi.adapter(SpecialCollections::class.java)
val specialCollections = SpecialCollections(
mutableListOf(),
mutableSetOf(),
mutableMapOf(),
emptyList(),
emptySet(),
emptyMap()
)
val json = adapter.toJson(specialCollections)
val newCollections = adapter.fromJson(json)
assertThat(newCollections).isEqualTo(specialCollections)
}
@MoshiSerializable
data class SpecialCollections(
val mutableList: MutableList<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)
}
@MoshiSerializable
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
)
val noNullsTypeParams = NullableTypeParams<Int>(
nullableTypeParams.nullableList,
nullableTypeParams.nullableSet,
nullableTypeParams.nullableMap.filterValues { it != null },
null
)
val json = adapter.toJson(nullableTypeParams)
val newNullableTypeParams = adapter.fromJson(json)
assertThat(newNullableTypeParams).isEqualTo(noNullsTypeParams)
val nullSerializedJson = nullSerializing.toJson(nullableTypeParams)
val nullSerializedNullableTypeParams = adapter.fromJson(nullSerializedJson)
assertThat(nullSerializedNullableTypeParams).isEqualTo(nullableTypeParams)
}
}
// Has to be outside to avoid Types seeing an owning class
@MoshiSerializable
data class NullableTypeParams<T>(
val nullableList: List<String?>,
val nullableSet: Set<String?>,
val nullableMap: Map<String, String?>,
val nullableT: T?
)
typealias TypeAliasName = String
/**
* This is here mostly just to ensure it still compiles. Covers variance, @Json, default values,
* nullability, primitive arrays, and some wacky generics.
*/
@MoshiSerializable
data class SmokeTestType(
@Json(name = "first_name") val firstName: String,
@Json(name = "last_name") val lastName: String,
val age: Int,
val nationalities: List<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"
)

View File

@@ -0,0 +1,760 @@
/*
* Copyright (C) 2017 Square, Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.squareup.moshi
import org.assertj.core.api.Assertions.assertThat
import org.junit.Assert.fail
import org.junit.Ignore
import org.junit.Test
import java.io.ByteArrayOutputStream
import java.util.Locale
import java.util.SimpleTimeZone
import kotlin.annotation.AnnotationRetention.RUNTIME
class KotlinCodeGenTest {
@Ignore @Test fun constructorParameters() {
val moshi = Moshi.Builder().add(MoshiSerializableFactory()).build()
val jsonAdapter = moshi.adapter(ConstructorParameters::class.java)
val encoded = ConstructorParameters(3, 5)
assertThat(jsonAdapter.toJson(encoded)).isEqualTo("""{"a":3,"b":5}""")
val decoded = jsonAdapter.fromJson("""{"a":4,"b":6}""")!!
assertThat(decoded.a).isEqualTo(4)
assertThat(decoded.b).isEqualTo(6)
}
class ConstructorParameters(var a: Int, var b: Int)
@Ignore @Test fun properties() {
val moshi = Moshi.Builder().add(MoshiSerializableFactory()).build()
val jsonAdapter = moshi.adapter(Properties::class.java)
val encoded = Properties()
encoded.a = 3
encoded.b = 5
assertThat(jsonAdapter.toJson(encoded)).isEqualTo("""{"a":3,"b":5}""")
val decoded = jsonAdapter.fromJson("""{"a":3,"b":5}""")!!
assertThat(decoded.a).isEqualTo(3)
assertThat(decoded.b).isEqualTo(5)
}
class Properties {
var a: Int = -1
var b: Int = -1
}
@Ignore @Test fun constructorParametersAndProperties() {
val moshi = Moshi.Builder().add(MoshiSerializableFactory()).build()
val jsonAdapter = moshi.adapter(ConstructorParametersAndProperties::class.java)
val encoded = ConstructorParametersAndProperties(3)
encoded.b = 5
assertThat(jsonAdapter.toJson(encoded)).isEqualTo("""{"a":3,"b":5}""")
val decoded = jsonAdapter.fromJson("""{"a":4,"b":6}""")!!
assertThat(decoded.a).isEqualTo(4)
assertThat(decoded.b).isEqualTo(6)
}
class ConstructorParametersAndProperties(var a: Int) {
var b: Int = -1
}
@Ignore @Test fun immutableConstructorParameters() {
val moshi = Moshi.Builder().add(MoshiSerializableFactory()).build()
val jsonAdapter = moshi.adapter(ImmutableConstructorParameters::class.java)
val encoded = ImmutableConstructorParameters(3, 5)
assertThat(jsonAdapter.toJson(encoded)).isEqualTo("""{"a":3,"b":5}""")
val decoded = jsonAdapter.fromJson("""{"a":4,"b":6}""")!!
assertThat(decoded.a).isEqualTo(4)
assertThat(decoded.b).isEqualTo(6)
}
class ImmutableConstructorParameters(val a: Int, val b: Int)
@Ignore @Test fun immutableProperties() {
val moshi = Moshi.Builder().add(MoshiSerializableFactory()).build()
val jsonAdapter = moshi.adapter(ImmutableProperties::class.java)
val encoded = ImmutableProperties(3, 5)
assertThat(jsonAdapter.toJson(encoded)).isEqualTo("""{"a":3,"b":5}""")
val decoded = jsonAdapter.fromJson("""{"a":3,"b":5}""")!!
assertThat(decoded.a).isEqualTo(3)
assertThat(decoded.b).isEqualTo(5)
}
class ImmutableProperties(a: Int, b: Int) {
val a = a
val b = b
}
@Ignore @Test fun constructorDefaults() {
val moshi = Moshi.Builder().add(MoshiSerializableFactory()).build()
val jsonAdapter = moshi.adapter(ConstructorDefaultValues::class.java)
val encoded = ConstructorDefaultValues(3, 5)
assertThat(jsonAdapter.toJson(encoded)).isEqualTo("""{"a":3,"b":5}""")
val decoded = jsonAdapter.fromJson("""{"b":6}""")!!
assertThat(decoded.a).isEqualTo(-1)
assertThat(decoded.b).isEqualTo(6)
}
class ConstructorDefaultValues(var a: Int = -1, var b: Int = -2)
@Ignore @Test fun requiredValueAbsent() {
val moshi = Moshi.Builder().add(MoshiSerializableFactory()).build()
val jsonAdapter = moshi.adapter(RequiredValueAbsent::class.java)
try {
jsonAdapter.fromJson("""{"a":4}""")
fail()
} catch(expected: JsonDataException) {
assertThat(expected).hasMessage("Required value b missing at $")
}
}
class RequiredValueAbsent(var a: Int = 3, var b: Int)
@Ignore @Test fun nonNullConstructorParameterCalledWithNullFailsWithJsonDataException() {
val moshi = Moshi.Builder().add(MoshiSerializableFactory()).build()
val jsonAdapter = moshi.adapter(HasNonNullConstructorParameter::class.java)
try {
jsonAdapter.fromJson("{\"a\":null}")
fail()
} catch (expected: JsonDataException) {
assertThat(expected).hasMessage("Non-null value a was null at \$")
}
}
class HasNonNullConstructorParameter(val a: String)
@Ignore @Test fun nonNullPropertySetToNullFailsWithJsonDataException() {
val moshi = Moshi.Builder().add(MoshiSerializableFactory()).build()
val jsonAdapter = moshi.adapter(HasNonNullProperty::class.java)
try {
jsonAdapter.fromJson("{\"a\":null}")
fail()
} catch (expected: JsonDataException) {
assertThat(expected).hasMessage("Non-null value a was null at \$")
}
}
class HasNonNullProperty {
var a: String = ""
}
@Ignore @Test fun duplicatedValue() {
val moshi = Moshi.Builder().add(MoshiSerializableFactory()).build()
val jsonAdapter = moshi.adapter(DuplicateValue::class.java)
try {
jsonAdapter.fromJson("""{"a":4,"a":4}""")
fail()
} catch(expected: JsonDataException) {
assertThat(expected).hasMessage("Multiple values for a at $.a")
}
}
class DuplicateValue(var a: Int = -1, var b: Int = -2)
@Ignore @Test fun explicitNull() {
val moshi = Moshi.Builder().add(MoshiSerializableFactory()).build()
val jsonAdapter = moshi.adapter(ExplicitNull::class.java)
val encoded = ExplicitNull(null, 5)
assertThat(jsonAdapter.toJson(encoded)).isEqualTo("""{"b":5}""")
assertThat(jsonAdapter.serializeNulls().toJson(encoded)).isEqualTo("""{"a":null,"b":5}""")
val decoded = jsonAdapter.fromJson("""{"a":null,"b":6}""")!!
assertThat(decoded.a).isEqualTo(null)
assertThat(decoded.b).isEqualTo(6)
}
class ExplicitNull(var a: Int?, var b: Int?)
@Ignore @Test fun absentNull() {
val moshi = Moshi.Builder().add(MoshiSerializableFactory()).build()
val jsonAdapter = moshi.adapter(AbsentNull::class.java)
val encoded = AbsentNull(null, 5)
assertThat(jsonAdapter.toJson(encoded)).isEqualTo("""{"b":5}""")
assertThat(jsonAdapter.serializeNulls().toJson(encoded)).isEqualTo("""{"a":null,"b":5}""")
val decoded = jsonAdapter.fromJson("""{"b":6}""")!!
assertThat(decoded.a).isNull()
assertThat(decoded.b).isEqualTo(6)
}
class AbsentNull(var a: Int?, var b: Int?)
@Ignore @Test fun repeatedValue() {
val moshi = Moshi.Builder().add(MoshiSerializableFactory()).build()
val jsonAdapter = moshi.adapter(RepeatedValue::class.java)
try {
jsonAdapter.fromJson("""{"a":4,"b":null,"b":6}""")
fail()
} catch(expected: JsonDataException) {
assertThat(expected).hasMessage("Multiple values for b at $.b")
}
}
class RepeatedValue(var a: Int, var b: Int?)
@Ignore @Test fun constructorParameterWithQualifier() {
val moshi = Moshi.Builder()
.add(MoshiSerializableFactory())
.add(UppercaseJsonAdapter())
.build()
val jsonAdapter = moshi.adapter(ConstructorParameterWithQualifier::class.java)
val encoded = ConstructorParameterWithQualifier("Android", "Banana")
assertThat(jsonAdapter.toJson(encoded)).isEqualTo("""{"a":"ANDROID","b":"Banana"}""")
val decoded = jsonAdapter.fromJson("""{"a":"Android","b":"Banana"}""")!!
assertThat(decoded.a).isEqualTo("android")
assertThat(decoded.b).isEqualTo("Banana")
}
class ConstructorParameterWithQualifier(@Uppercase var a: String, var b: String)
@Ignore @Test fun propertyWithQualifier() {
val moshi = Moshi.Builder()
.add(MoshiSerializableFactory())
.add(UppercaseJsonAdapter())
.build()
val jsonAdapter = moshi.adapter(PropertyWithQualifier::class.java)
val encoded = PropertyWithQualifier()
encoded.a = "Android"
encoded.b = "Banana"
assertThat(jsonAdapter.toJson(encoded)).isEqualTo("""{"a":"ANDROID","b":"Banana"}""")
val decoded = jsonAdapter.fromJson("""{"a":"Android","b":"Banana"}""")!!
assertThat(decoded.a).isEqualTo("android")
assertThat(decoded.b).isEqualTo("Banana")
}
class PropertyWithQualifier {
@Uppercase var a: String = ""
var b: String = ""
}
@Ignore @Test fun constructorParameterWithJsonName() {
val moshi = Moshi.Builder().add(MoshiSerializableFactory()).build()
val jsonAdapter = moshi.adapter(ConstructorParameterWithJsonName::class.java)
val encoded = ConstructorParameterWithJsonName(3, 5)
assertThat(jsonAdapter.toJson(encoded)).isEqualTo("""{"key a":3,"b":5}""")
val decoded = jsonAdapter.fromJson("""{"key a":4,"b":6}""")!!
assertThat(decoded.a).isEqualTo(4)
assertThat(decoded.b).isEqualTo(6)
}
class ConstructorParameterWithJsonName(@Json(name = "key a") var a: Int, var b: Int)
@Ignore @Test fun propertyWithJsonName() {
val moshi = Moshi.Builder().add(MoshiSerializableFactory()).build()
val jsonAdapter = moshi.adapter(PropertyWithJsonName::class.java)
val encoded = PropertyWithJsonName()
encoded.a = 3
encoded.b = 5
assertThat(jsonAdapter.toJson(encoded)).isEqualTo("""{"key a":3,"b":5}""")
val decoded = jsonAdapter.fromJson("""{"key a":4,"b":6}""")!!
assertThat(decoded.a).isEqualTo(4)
assertThat(decoded.b).isEqualTo(6)
}
class PropertyWithJsonName {
@Json(name = "key a") var a: Int = -1
var b: Int = -1
}
@Ignore @Test fun transientConstructorParameter() {
val moshi = Moshi.Builder().add(MoshiSerializableFactory()).build()
val jsonAdapter = moshi.adapter(TransientConstructorParameter::class.java)
val encoded = TransientConstructorParameter(3, 5)
assertThat(jsonAdapter.toJson(encoded)).isEqualTo("""{"b":5}""")
val decoded = jsonAdapter.fromJson("""{"a":4,"b":6}""")!!
assertThat(decoded.a).isEqualTo(-1)
assertThat(decoded.b).isEqualTo(6)
}
class TransientConstructorParameter(@Transient var a: Int = -1, var b: Int = -1)
@Ignore @Test fun requiredTransientConstructorParameterFails() {
val moshi = Moshi.Builder().add(MoshiSerializableFactory()).build()
try {
moshi.adapter(RequiredTransientConstructorParameter::class.java)
fail()
} catch (expected: IllegalArgumentException) {
assertThat(expected).hasMessage("No default value for transient constructor parameter #0 " +
"a of fun <init>(kotlin.Int): " +
"com.squareup.moshi.KotlinJsonAdapterTest.RequiredTransientConstructorParameter")
}
}
class RequiredTransientConstructorParameter(@Transient var a: Int)
@Ignore @Test fun transientProperty() {
val moshi = Moshi.Builder().add(MoshiSerializableFactory()).build()
val jsonAdapter = moshi.adapter(TransientProperty::class.java)
val encoded = TransientProperty()
encoded.a = 3
encoded.b = 5
assertThat(jsonAdapter.toJson(encoded)).isEqualTo("""{"b":5}""")
val decoded = jsonAdapter.fromJson("""{"a":4,"b":6}""")!!
assertThat(decoded.a).isEqualTo(-1)
assertThat(decoded.b).isEqualTo(6)
}
class TransientProperty {
@Transient var a: Int = -1
var b: Int = -1
}
@Ignore @Test fun supertypeConstructorParameters() {
val moshi = Moshi.Builder().add(MoshiSerializableFactory()).build()
val jsonAdapter = moshi.adapter(SubtypeConstructorParameters::class.java)
val encoded = SubtypeConstructorParameters(3, 5)
assertThat(jsonAdapter.toJson(encoded)).isEqualTo("""{"a":3,"b":5}""")
val decoded = jsonAdapter.fromJson("""{"a":4,"b":6}""")!!
assertThat(decoded.a).isEqualTo(4)
assertThat(decoded.b).isEqualTo(6)
}
open class SupertypeConstructorParameters(var a: Int)
class SubtypeConstructorParameters(a: Int, var b: Int) : SupertypeConstructorParameters(a)
@Ignore @Test fun supertypeProperties() {
val moshi = Moshi.Builder().add(MoshiSerializableFactory()).build()
val jsonAdapter = moshi.adapter(SubtypeProperties::class.java)
val encoded = SubtypeProperties()
encoded.a = 3
encoded.b = 5
assertThat(jsonAdapter.toJson(encoded)).isEqualTo("""{"b":5,"a":3}""")
val decoded = jsonAdapter.fromJson("""{"a":4,"b":6}""")!!
assertThat(decoded.a).isEqualTo(4)
assertThat(decoded.b).isEqualTo(6)
}
open class SupertypeProperties {
var a: Int = -1
}
class SubtypeProperties : SupertypeProperties() {
var b: Int = -1
}
@Ignore @Test fun extendsPlatformClassWithPrivateField() {
val moshi = Moshi.Builder().add(MoshiSerializableFactory()).build()
val jsonAdapter = moshi.adapter(ExtendsPlatformClassWithPrivateField::class.java)
val encoded = ExtendsPlatformClassWithPrivateField(3)
assertThat(jsonAdapter.toJson(encoded)).isEqualTo("""{"a":3}""")
val decoded = jsonAdapter.fromJson("""{"a":4,"id":"B"}""")!!
assertThat(decoded.a).isEqualTo(4)
assertThat(decoded.id).isEqualTo("C")
}
internal class ExtendsPlatformClassWithPrivateField(var a: Int) : SimpleTimeZone(0, "C")
@Ignore @Test fun extendsPlatformClassWithProtectedField() {
val moshi = Moshi.Builder().add(MoshiSerializableFactory()).build()
val jsonAdapter = moshi.adapter(ExtendsPlatformClassWithProtectedField::class.java)
val encoded = ExtendsPlatformClassWithProtectedField(3)
assertThat(jsonAdapter.toJson(encoded)).isEqualTo("""{"a":3,"buf":[0,0],"count":0}""")
val decoded = jsonAdapter.fromJson("""{"a":4,"buf":[0,0],"size":0}""")!!
assertThat(decoded.a).isEqualTo(4)
assertThat(decoded.buf()).isEqualTo(ByteArray(2, { 0 }))
assertThat(decoded.count()).isEqualTo(0)
}
internal class ExtendsPlatformClassWithProtectedField(var a: Int) : ByteArrayOutputStream(2) {
fun buf() = buf
fun count() = count
}
@Ignore @Test fun platformTypeThrows() {
val moshi = Moshi.Builder().add(MoshiSerializableFactory()).build()
try {
moshi.adapter(Triple::class.java)
fail()
} catch (e: IllegalArgumentException) {
assertThat(e).hasMessage("Platform class kotlin.Triple annotated [] "
+ "requires explicit JsonAdapter to be registered")
}
}
@Ignore @Test fun privateConstructorParameters() {
val moshi = Moshi.Builder().add(MoshiSerializableFactory()).build()
val jsonAdapter = moshi.adapter(PrivateConstructorParameters::class.java)
val encoded = PrivateConstructorParameters(3, 5)
assertThat(jsonAdapter.toJson(encoded)).isEqualTo("""{"a":3,"b":5}""")
val decoded = jsonAdapter.fromJson("""{"a":4,"b":6}""")!!
assertThat(decoded.a()).isEqualTo(4)
assertThat(decoded.b()).isEqualTo(6)
}
class PrivateConstructorParameters(private var a: Int, private var b: Int) {
fun a() = a
fun b() = b
}
@Ignore @Test fun privateConstructor() {
val moshi = Moshi.Builder().add(MoshiSerializableFactory()).build()
val jsonAdapter = moshi.adapter(PrivateConstructor::class.java)
val encoded = PrivateConstructor.newInstance(3, 5)
assertThat(jsonAdapter.toJson(encoded)).isEqualTo("""{"a":3,"b":5}""")
val decoded = jsonAdapter.fromJson("""{"a":4,"b":6}""")!!
assertThat(decoded.a()).isEqualTo(4)
assertThat(decoded.b()).isEqualTo(6)
}
class PrivateConstructor private constructor(var a: Int, var b: Int) {
fun a() = a
fun b() = b
companion object {
fun newInstance(a: Int, b: Int) = PrivateConstructor(a, b)
}
}
@Ignore @Test fun privateProperties() {
val moshi = Moshi.Builder().add(MoshiSerializableFactory()).build()
val jsonAdapter = moshi.adapter(PrivateProperties::class.java)
val encoded = PrivateProperties()
encoded.a(3)
encoded.b(5)
assertThat(jsonAdapter.toJson(encoded)).isEqualTo("""{"a":3,"b":5}""")
val decoded = jsonAdapter.fromJson("""{"a":4,"b":6}""")!!
assertThat(decoded.a()).isEqualTo(4)
assertThat(decoded.b()).isEqualTo(6)
}
class PrivateProperties {
var a: Int = -1
var b: Int = -1
fun a() = a
fun a(a: Int) {
this.a = a
}
fun b() = b
fun b(b: Int) {
this.b = b
}
}
@Ignore @Test fun unsettablePropertyIgnored() {
val moshi = Moshi.Builder().add(MoshiSerializableFactory()).build()
val jsonAdapter = moshi.adapter(UnsettableProperty::class.java)
val encoded = UnsettableProperty()
encoded.b = 5
assertThat(jsonAdapter.toJson(encoded)).isEqualTo("""{"b":5}""")
val decoded = jsonAdapter.fromJson("""{"a":4,"b":6}""")!!
assertThat(decoded.a).isEqualTo(-1)
assertThat(decoded.b).isEqualTo(6)
}
class UnsettableProperty {
val a: Int = -1
var b: Int = -1
}
@Ignore @Test fun getterOnlyNoBackingField() {
val moshi = Moshi.Builder().add(MoshiSerializableFactory()).build()
val jsonAdapter = moshi.adapter(GetterOnly::class.java)
val encoded = GetterOnly(3, 5)
assertThat(jsonAdapter.toJson(encoded)).isEqualTo("""{"a":3,"b":5}""")
val decoded = jsonAdapter.fromJson("""{"a":4,"b":6}""")!!
assertThat(decoded.a).isEqualTo(4)
assertThat(decoded.b).isEqualTo(6)
assertThat(decoded.total).isEqualTo(10)
}
class GetterOnly(var a: Int, var b: Int) {
val total : Int
get() = a + b
}
@Ignore @Test fun getterAndSetterNoBackingField() {
val moshi = Moshi.Builder().add(MoshiSerializableFactory()).build()
val jsonAdapter = moshi.adapter(GetterAndSetter::class.java)
val encoded = GetterAndSetter(3, 5)
assertThat(jsonAdapter.toJson(encoded)).isEqualTo("""{"a":3,"b":5,"total":8}""")
// Whether b is 6 or 7 is an implementation detail. Currently we call constructors then setters.
val decoded1 = jsonAdapter.fromJson("""{"a":4,"b":6,"total":11}""")!!
assertThat(decoded1.a).isEqualTo(4)
assertThat(decoded1.b).isEqualTo(7)
assertThat(decoded1.total).isEqualTo(11)
// Whether b is 6 or 7 is an implementation detail. Currently we call constructors then setters.
val decoded2 = jsonAdapter.fromJson("""{"a":4,"total":11,"b":6}""")!!
assertThat(decoded2.a).isEqualTo(4)
assertThat(decoded2.b).isEqualTo(7)
assertThat(decoded2.total).isEqualTo(11)
}
class GetterAndSetter(var a: Int, var b: Int) {
var total : Int
get() = a + b
set(value) {
b = value - a
}
}
@Ignore @Test fun nonPropertyConstructorParameter() {
val moshi = Moshi.Builder().add(MoshiSerializableFactory()).build()
try {
moshi.adapter(NonPropertyConstructorParameter::class.java)
fail()
} catch(expected: IllegalArgumentException) {
assertThat(expected).hasMessage(
"No property for required constructor parameter #0 a of fun <init>(" +
"kotlin.Int, kotlin.Int): ${NonPropertyConstructorParameter::class.qualifiedName}")
}
}
class NonPropertyConstructorParameter(a: Int, val b: Int)
@Ignore @Test fun kotlinEnumsAreNotCovered() {
val moshi = Moshi.Builder().add(MoshiSerializableFactory()).build()
val adapter = moshi.adapter(UsingEnum::class.java)
assertThat(adapter.fromJson("""{"e": "A"}""")).isEqualTo(UsingEnum(KotlinEnum.A))
}
data class UsingEnum(val e: KotlinEnum)
enum class KotlinEnum {
A, B
}
@Ignore @Test fun interfacesNotSupported() {
val moshi = Moshi.Builder().add(MoshiSerializableFactory()).build()
try {
moshi.adapter(Interface::class.java)
fail()
} catch (e: IllegalArgumentException) {
assertThat(e).hasMessage("No JsonAdapter for interface " +
"com.squareup.moshi.KotlinJsonAdapterTest\$Interface annotated []")
}
}
interface Interface
@Ignore @Test fun abstractClassesNotSupported() {
val moshi = Moshi.Builder().add(MoshiSerializableFactory()).build()
try {
moshi.adapter(AbstractClass::class.java)
fail()
} catch (e: IllegalArgumentException) {
assertThat(e).hasMessage(
"Cannot serialize abstract class com.squareup.moshi.KotlinJsonAdapterTest\$AbstractClass")
}
}
abstract class AbstractClass(val a: Int)
@Ignore @Test fun innerClassesNotSupported() {
val moshi = Moshi.Builder().add(MoshiSerializableFactory()).build()
try {
moshi.adapter(InnerClass::class.java)
fail()
} catch (e: IllegalArgumentException) {
assertThat(e).hasMessage(
"Cannot serialize non-static nested class com.squareup.moshi.KotlinCodeGenTest\$InnerClass")
}
}
inner class InnerClass(val a: Int)
@Ignore @Test fun localClassesNotSupported() {
class LocalClass(val a: Int)
val moshi = Moshi.Builder().add(MoshiSerializableFactory()).build()
try {
moshi.adapter(LocalClass::class.java)
fail()
} catch (e: IllegalArgumentException) {
assertThat(e).hasMessage("Cannot serialize local class or object expression " +
"com.squareup.moshi.KotlinJsonAdapterTest\$localClassesNotSupported\$LocalClass")
}
}
@Ignore @Test fun objectDeclarationsNotSupported() {
val moshi = Moshi.Builder().add(MoshiSerializableFactory()).build()
try {
moshi.adapter(ObjectDeclaration.javaClass)
fail()
} catch (e: IllegalArgumentException) {
assertThat(e).hasMessage("Cannot serialize object declaration " +
"com.squareup.moshi.KotlinJsonAdapterTest\$ObjectDeclaration")
}
}
object ObjectDeclaration {
var a = 5
}
@Ignore @Test fun objectExpressionsNotSupported() {
val expression = object : Any() {
var a = 5
}
val moshi = Moshi.Builder().add(MoshiSerializableFactory()).build()
try {
moshi.adapter(expression.javaClass)
fail()
} catch (e: IllegalArgumentException) {
assertThat(e).hasMessage("Cannot serialize local class or object expression " +
"com.squareup.moshi.KotlinJsonAdapterTest\$objectExpressionsNotSupported\$expression$1")
}
}
@Ignore @Test fun manyProperties32() {
val moshi = Moshi.Builder().add(MoshiSerializableFactory()).build()
val jsonAdapter = moshi.adapter(ManyProperties32::class.java)
val encoded = ManyProperties32(
101, 102, 103, 104, 105,
106, 107, 108, 109, 110,
111, 112, 113, 114, 115,
116, 117, 118, 119, 120,
121, 122, 123, 124, 125,
126, 127, 128, 129, 130,
131, 132)
val json = ("""
|{
|"v01":101,"v02":102,"v03":103,"v04":104,"v05":105,
|"v06":106,"v07":107,"v08":108,"v09":109,"v10":110,
|"v11":111,"v12":112,"v13":113,"v14":114,"v15":115,
|"v16":116,"v17":117,"v18":118,"v19":119,"v20":120,
|"v21":121,"v22":122,"v23":123,"v24":124,"v25":125,
|"v26":126,"v27":127,"v28":128,"v29":129,"v30":130,
|"v31":131,"v32":132
|}
|""").trimMargin().replace("\n", "")
assertThat(jsonAdapter.toJson(encoded)).isEqualTo(json)
val decoded = jsonAdapter.fromJson(json)!!
assertThat(decoded.v01).isEqualTo(101)
assertThat(decoded.v32).isEqualTo(132)
}
class ManyProperties32(
var v01: Int, var v02: Int, var v03: Int, var v04: Int, var v05: Int,
var v06: Int, var v07: Int, var v08: Int, var v09: Int, var v10: Int,
var v11: Int, var v12: Int, var v13: Int, var v14: Int, var v15: Int,
var v16: Int, var v17: Int, var v18: Int, var v19: Int, var v20: Int,
var v21: Int, var v22: Int, var v23: Int, var v24: Int, var v25: Int,
var v26: Int, var v27: Int, var v28: Int, var v29: Int, var v30: Int,
var v31: Int, var v32: Int)
@Ignore @Test fun manyProperties33() {
val moshi = Moshi.Builder().add(MoshiSerializableFactory()).build()
val jsonAdapter = moshi.adapter(ManyProperties33::class.java)
val encoded = ManyProperties33(
101, 102, 103, 104, 105,
106, 107, 108, 109, 110,
111, 112, 113, 114, 115,
116, 117, 118, 119, 120,
121, 122, 123, 124, 125,
126, 127, 128, 129, 130,
131, 132, 133)
val json = ("""
|{
|"v01":101,"v02":102,"v03":103,"v04":104,"v05":105,
|"v06":106,"v07":107,"v08":108,"v09":109,"v10":110,
|"v11":111,"v12":112,"v13":113,"v14":114,"v15":115,
|"v16":116,"v17":117,"v18":118,"v19":119,"v20":120,
|"v21":121,"v22":122,"v23":123,"v24":124,"v25":125,
|"v26":126,"v27":127,"v28":128,"v29":129,"v30":130,
|"v31":131,"v32":132,"v33":133
|}
|""").trimMargin().replace("\n", "")
assertThat(jsonAdapter.toJson(encoded)).isEqualTo(json)
val decoded = jsonAdapter.fromJson(json)!!
assertThat(decoded.v01).isEqualTo(101)
assertThat(decoded.v32).isEqualTo(132)
assertThat(decoded.v33).isEqualTo(133)
}
class ManyProperties33(
var v01: Int, var v02: Int, var v03: Int, var v04: Int, var v05: Int,
var v06: Int, var v07: Int, var v08: Int, var v09: Int, var v10: Int,
var v11: Int, var v12: Int, var v13: Int, var v14: Int, var v15: Int,
var v16: Int, var v17: Int, var v18: Int, var v19: Int, var v20: Int,
var v21: Int, var v22: Int, var v23: Int, var v24: Int, var v25: Int,
var v26: Int, var v27: Int, var v28: Int, var v29: Int, var v30: Int,
var v31: Int, var v32: Int, var v33: Int)
// TODO(jwilson): resolve generic types?
@Retention(RUNTIME)
@JsonQualifier
annotation class Uppercase
class UppercaseJsonAdapter {
@ToJson fun toJson(@Uppercase s: String) : String {
return s.toUpperCase(Locale.US)
}
@FromJson @Uppercase fun fromJson(s: String) : String {
return s.toLowerCase(Locale.US)
}
}
}

View File

@@ -0,0 +1,82 @@
<?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-runtime</artifactId>
<dependencies>
<dependency>
<groupId>com.squareup.moshi</groupId>
<artifactId>moshi</artifactId>
<version>${project.version}</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>
<dependency>
<groupId>org.jetbrains.kotlin</groupId>
<artifactId>kotlin-stdlib</artifactId>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.jetbrains.kotlin</groupId>
<artifactId>kotlin-maven-plugin</artifactId>
<version>${kotlin.version}</version>
<executions>
<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>
</plugins>
</build>
</project>

View File

@@ -0,0 +1,77 @@
/*
* Copyright (C) 2018 Square, Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.squareup.moshi
import java.lang.reflect.InvocationTargetException
import java.lang.reflect.ParameterizedType
import java.lang.reflect.Type
import kotlin.annotation.AnnotationRetention.RUNTIME
import kotlin.annotation.AnnotationTarget.CLASS
@Retention(RUNTIME)
@Target(CLASS)
annotation class MoshiSerializable
class MoshiSerializableFactory : JsonAdapter.Factory {
override fun create(type: Type, annotations: Set<Annotation>, moshi: Moshi): JsonAdapter<*>? {
val rawType = Types.getRawType(type)
if (!rawType.isAnnotationPresent(MoshiSerializable::class.java)) {
return null
}
val clsName = rawType.name.replace("$", "_")
val constructor = try {
@Suppress("UNCHECKED_CAST")
val bindingClass = rawType.classLoader
.loadClass(clsName + "JsonAdapter") as Class<out JsonAdapter<*>>
if (type is ParameterizedType) {
// This is generic, use the two param moshi + type constructor
bindingClass.getDeclaredConstructor(Moshi::class.java, Array<Type>::class.java)
} else {
// The standard single param moshi constructor
bindingClass.getDeclaredConstructor(Moshi::class.java)
}
} catch (e: ClassNotFoundException) {
throw RuntimeException("Unable to find generated Moshi adapter class for $clsName", e)
} catch (e: NoSuchMethodException) {
throw RuntimeException("Unable to find generated Moshi adapter constructor for $clsName", e)
}
try {
return when {
constructor.parameterTypes.size == 1 -> constructor.newInstance(moshi)
type is ParameterizedType -> constructor.newInstance(moshi, type.actualTypeArguments)
else -> throw IllegalStateException("Unable to handle type $type")
}
} catch (e: IllegalAccessException) {
throw RuntimeException("Unable to invoke $constructor", e)
} catch (e: InstantiationException) {
throw RuntimeException("Unable to invoke $constructor", e)
} catch (e: InvocationTargetException) {
val cause = e.cause
if (cause is RuntimeException) {
throw cause
}
if (cause is Error) {
throw cause
}
throw RuntimeException(
"Could not create generated JsonAdapter instance for type $rawType", cause)
}
}
}

View File

@@ -47,6 +47,7 @@
<plugin> <plugin>
<groupId>org.jetbrains.kotlin</groupId> <groupId>org.jetbrains.kotlin</groupId>
<artifactId>kotlin-maven-plugin</artifactId> <artifactId>kotlin-maven-plugin</artifactId>
<version>${kotlin.version}</version>
<executions> <executions>
<execution> <execution>
<id>compile</id> <id>compile</id>

View File

@@ -23,6 +23,7 @@ import java.util.ArrayList;
import java.util.Arrays; import java.util.Arrays;
import java.util.Collections; import java.util.Collections;
import java.util.LinkedHashMap; import java.util.LinkedHashMap;
import java.util.LinkedHashSet;
import java.util.List; import java.util.List;
import java.util.Map; import java.util.Map;
import java.util.Set; import java.util.Set;
@@ -73,6 +74,18 @@ public final class Moshi {
Collections.singleton(Types.createJsonQualifierImplementation(annotationType))); Collections.singleton(Types.createJsonQualifierImplementation(annotationType)));
} }
@CheckReturnValue
public <T> JsonAdapter<T> adapter(Type type, Class<? extends Annotation>... annotationTypes) {
if (annotationTypes.length == 1) {
return adapter(type, annotationTypes[0]);
}
Set<Annotation> annotations = new LinkedHashSet<>(annotationTypes.length);
for (Class<? extends Annotation> annotationType : annotationTypes) {
annotations.add(Types.createJsonQualifierImplementation(annotationType));
}
return adapter(type, Collections.unmodifiableSet(annotations));
}
@CheckReturnValue @CheckReturnValue
@SuppressWarnings("unchecked") // Factories are required to return only matching JsonAdapters. @SuppressWarnings("unchecked") // Factories are required to return only matching JsonAdapters.
public <T> JsonAdapter<T> adapter(Type type, Set<? extends Annotation> annotations) { public <T> JsonAdapter<T> adapter(Type type, Set<? extends Annotation> annotations) {

View File

@@ -22,12 +22,15 @@
<module>examples</module> <module>examples</module>
<module>adapters</module> <module>adapters</module>
<module>kotlin</module> <module>kotlin</module>
<module>kotlin-codegen/compiler</module>
<module>kotlin-codegen/integration-test</module>
<module>kotlin-codegen/runtime</module>
</modules> </modules>
<properties> <properties>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding> <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<java.version>1.7</java.version> <java.version>1.7</java.version>
<kotlin.version>1.1.60</kotlin.version> <kotlin.version>1.2.21</kotlin.version>
<!-- Dependencies --> <!-- Dependencies -->
<okio.version>1.13.0</okio.version> <okio.version>1.13.0</okio.version>