mirror of
https://github.com/fankes/moshi.git
synced 2025-10-20 00:19:21 +08:00
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:
134
kotlin-codegen/compiler/pom.xml
Normal file
134
kotlin-codegen/compiler/pom.xml
Normal 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>
|
@@ -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
|
||||
}
|
153
kotlin-codegen/integration-test/pom.xml
Normal file
153
kotlin-codegen/integration-test/pom.xml
Normal 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>
|
@@ -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"
|
||||
)
|
@@ -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)
|
||||
}
|
||||
}
|
||||
}
|
82
kotlin-codegen/runtime/pom.xml
Normal file
82
kotlin-codegen/runtime/pom.xml
Normal 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>
|
@@ -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)
|
||||
}
|
||||
}
|
||||
}
|
@@ -47,6 +47,7 @@
|
||||
<plugin>
|
||||
<groupId>org.jetbrains.kotlin</groupId>
|
||||
<artifactId>kotlin-maven-plugin</artifactId>
|
||||
<version>${kotlin.version}</version>
|
||||
<executions>
|
||||
<execution>
|
||||
<id>compile</id>
|
||||
|
@@ -23,6 +23,7 @@ import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.Collections;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.LinkedHashSet;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
@@ -73,6 +74,18 @@ public final class Moshi {
|
||||
Collections.singleton(Types.createJsonQualifierImplementation(annotationType)));
|
||||
}
|
||||
|
||||
@CheckReturnValue
|
||||
public <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
|
||||
@SuppressWarnings("unchecked") // Factories are required to return only matching JsonAdapters.
|
||||
public <T> JsonAdapter<T> adapter(Type type, Set<? extends Annotation> annotations) {
|
||||
|
5
pom.xml
5
pom.xml
@@ -22,12 +22,15 @@
|
||||
<module>examples</module>
|
||||
<module>adapters</module>
|
||||
<module>kotlin</module>
|
||||
<module>kotlin-codegen/compiler</module>
|
||||
<module>kotlin-codegen/integration-test</module>
|
||||
<module>kotlin-codegen/runtime</module>
|
||||
</modules>
|
||||
|
||||
<properties>
|
||||
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
|
||||
<java.version>1.7</java.version>
|
||||
<kotlin.version>1.1.60</kotlin.version>
|
||||
<kotlin.version>1.2.21</kotlin.version>
|
||||
|
||||
<!-- Dependencies -->
|
||||
<okio.version>1.13.0</okio.version>
|
||||
|
Reference in New Issue
Block a user