From a24c00c2125803f9d62e7822462459d3cf5bce99 Mon Sep 17 00:00:00 2001 From: Nicklas Ansman Giertz Date: Thu, 28 Jan 2021 20:21:39 -0500 Subject: [PATCH] Implement support for generic sealed classes (#152) --- .../ansman/kotshi/AdaptersProcessingStep.kt | 23 +- .../se/ansman/kotshi/FactoryProcessingStep.kt | 11 +- .../se/ansman/kotshi/GeneratedAdapter.kt | 12 +- .../se/ansman/kotshi/KotshiProcessor.kt | 7 +- .../se/ansman/kotshi/MetadataAccessor.kt | 23 ++ .../se/ansman/kotshi/SealedClassSubtype.kt | 84 ++++++ .../kotlin/se/ansman/kotshi/TypeRenderer.kt | 114 ++++++++ .../kotshi/generators/AdapterGenerator.kt | 24 +- .../generators/DataClassAdapterGenerator.kt | 244 +++++++++--------- .../kotshi/generators/EnumAdapterGenerator.kt | 10 +- .../generators/ObjectAdapterGenerator.kt | 10 +- .../generators/SealedClassAdapterGenerator.kt | 130 ++++++---- .../kotshi/SealedClassWithComplexGeneric.kt | 13 + .../ansman/kotshi/SealedClassWithGeneric.kt | 13 + .../SealedClassWithComplexGenericTest.kt | 37 +++ .../kotshi/SealedClassWithGenericTest.kt | 36 +++ 16 files changed, 585 insertions(+), 206 deletions(-) create mode 100644 compiler/src/main/kotlin/se/ansman/kotshi/MetadataAccessor.kt create mode 100644 compiler/src/main/kotlin/se/ansman/kotshi/SealedClassSubtype.kt create mode 100644 compiler/src/main/kotlin/se/ansman/kotshi/TypeRenderer.kt create mode 100644 tests/src/main/kotlin/se/ansman/kotshi/SealedClassWithComplexGeneric.kt create mode 100644 tests/src/main/kotlin/se/ansman/kotshi/SealedClassWithGeneric.kt create mode 100644 tests/src/test/kotlin/se/ansman/kotshi/SealedClassWithComplexGenericTest.kt create mode 100644 tests/src/test/kotlin/se/ansman/kotshi/SealedClassWithGenericTest.kt diff --git a/compiler/src/main/kotlin/se/ansman/kotshi/AdaptersProcessingStep.kt b/compiler/src/main/kotlin/se/ansman/kotshi/AdaptersProcessingStep.kt index 1735448c..ca6d2125 100644 --- a/compiler/src/main/kotlin/se/ansman/kotshi/AdaptersProcessingStep.kt +++ b/compiler/src/main/kotlin/se/ansman/kotshi/AdaptersProcessingStep.kt @@ -8,7 +8,6 @@ import com.squareup.kotlinpoet.metadata.isData import com.squareup.kotlinpoet.metadata.isEnum import com.squareup.kotlinpoet.metadata.isObject import com.squareup.kotlinpoet.metadata.isSealed -import com.squareup.kotlinpoet.metadata.specs.ClassInspector import com.squareup.kotlinpoet.metadata.toImmutableKmClass import se.ansman.kotshi.generators.DataClassAdapterGenerator import se.ansman.kotshi.generators.EnumAdapterGenerator @@ -26,7 +25,7 @@ import javax.tools.Diagnostic class AdaptersProcessingStep( override val processor: KotshiProcessor, - private val classInspector: ClassInspector, + private val metadataAccessor: MetadataAccessor, private val messager: Messager, override val filer: Filer, private val adapters: MutableList, @@ -60,36 +59,40 @@ class AdaptersProcessingStep( val generator = when { metadata.isData -> DataClassAdapterGenerator( - classInspector = classInspector, + metadataAccessor = metadataAccessor, types = types, elements = elements, element = typeElement, metadata = metadata, - globalConfig = globalConfig + globalConfig = globalConfig, + messager = messager ) metadata.isEnum -> EnumAdapterGenerator( - classInspector = classInspector, + metadataAccessor = metadataAccessor, types = types, elements = elements, element = typeElement, metadata = metadata, - globalConfig = globalConfig + globalConfig = globalConfig, + messager = messager ) metadata.isObject -> ObjectAdapterGenerator( - classInspector = classInspector, + metadataAccessor = metadataAccessor, types = types, element = typeElement, metadata = metadata, elements = elements, - globalConfig = globalConfig + globalConfig = globalConfig, + messager = messager ) metadata.isSealed -> SealedClassAdapterGenerator( - classInspector = classInspector, + metadataAccessor = metadataAccessor, types = types, element = typeElement, metadata = metadata, elements = elements, - globalConfig = globalConfig + globalConfig = globalConfig, + messager = messager ) else -> throw ProcessingError( "@JsonSerializable can only be applied to enums, objects, sealed classes and data classes", diff --git a/compiler/src/main/kotlin/se/ansman/kotshi/FactoryProcessingStep.kt b/compiler/src/main/kotlin/se/ansman/kotshi/FactoryProcessingStep.kt index b92ee2e3..667e5f39 100644 --- a/compiler/src/main/kotlin/se/ansman/kotshi/FactoryProcessingStep.kt +++ b/compiler/src/main/kotlin/se/ansman/kotshi/FactoryProcessingStep.kt @@ -126,9 +126,14 @@ class FactoryProcessingStep( addCode(adapter.typeVariables.joinToString(", ", prefix = "<", postfix = ">") { "Nothing" }) } addCode("(") - when { - adapter.requiresTypes -> addCode("%N, %N.%M", moshiParam, typeParam, typeArgumentsOrFail) - adapter.requiresMoshi -> addCode("%N", moshiParam) + if (adapter.requiresMoshi) { + addCode("%N", moshiParam) + } + if (adapter.requiresTypes) { + if (adapter.requiresMoshi) { + addCode(", ") + } + addCode("%N.%M", typeParam, typeArgumentsOrFail) } addCode(")\n»") } diff --git a/compiler/src/main/kotlin/se/ansman/kotshi/GeneratedAdapter.kt b/compiler/src/main/kotlin/se/ansman/kotshi/GeneratedAdapter.kt index 62c3fef7..0819ef4b 100644 --- a/compiler/src/main/kotlin/se/ansman/kotshi/GeneratedAdapter.kt +++ b/compiler/src/main/kotlin/se/ansman/kotshi/GeneratedAdapter.kt @@ -7,12 +7,6 @@ data class GeneratedAdapter( val targetType: ClassName, val className: ClassName, val typeVariables: List, - val requiresMoshi: Boolean = true -) { - val requiresTypes: Boolean = typeVariables.isNotEmpty() - init { - assert(!requiresTypes || requiresMoshi) { - "An adapter requiring types must also require a Moshi instance." - } - } -} + val requiresTypes: Boolean, + val requiresMoshi: Boolean +) diff --git a/compiler/src/main/kotlin/se/ansman/kotshi/KotshiProcessor.kt b/compiler/src/main/kotlin/se/ansman/kotshi/KotshiProcessor.kt index 3093dc1a..31e37a8a 100644 --- a/compiler/src/main/kotlin/se/ansman/kotshi/KotshiProcessor.kt +++ b/compiler/src/main/kotlin/se/ansman/kotshi/KotshiProcessor.kt @@ -6,7 +6,6 @@ import com.google.common.collect.ImmutableSetMultimap import com.google.common.collect.Multimaps import com.google.common.collect.SetMultimap import com.squareup.kotlinpoet.classinspector.elements.ElementsClassInspector -import com.squareup.kotlinpoet.metadata.specs.ClassInspector import net.ltgt.gradle.incap.IncrementalAnnotationProcessor import net.ltgt.gradle.incap.IncrementalAnnotationProcessorType.AGGREGATING import javax.annotation.processing.AbstractProcessor @@ -25,7 +24,7 @@ import javax.lang.model.util.Types class KotshiProcessor : AbstractProcessor() { private lateinit var elements: Elements private lateinit var types: Types - private lateinit var classInspector: ClassInspector + private lateinit var metadataAccessor: MetadataAccessor private lateinit var steps: ImmutableList override fun getSupportedSourceVersion(): SourceVersion = SourceVersion.latestSupported() @@ -35,7 +34,7 @@ class KotshiProcessor : AbstractProcessor() { return listOf( AdaptersProcessingStep( processor = this, - classInspector = classInspector, + metadataAccessor = metadataAccessor, messager = processingEnv.messager, filer = processingEnv.filer, adapters = adapters, @@ -60,7 +59,7 @@ class KotshiProcessor : AbstractProcessor() { super.init(processingEnv) elements = processingEnv.elementUtils types = processingEnv.typeUtils - classInspector = ElementsClassInspector.create(elements, processingEnv.typeUtils) + metadataAccessor = MetadataAccessor(ElementsClassInspector.create(elements, processingEnv.typeUtils)) steps = ImmutableList.copyOf(initSteps()) } diff --git a/compiler/src/main/kotlin/se/ansman/kotshi/MetadataAccessor.kt b/compiler/src/main/kotlin/se/ansman/kotshi/MetadataAccessor.kt new file mode 100644 index 00000000..c2e28da2 --- /dev/null +++ b/compiler/src/main/kotlin/se/ansman/kotshi/MetadataAccessor.kt @@ -0,0 +1,23 @@ +package se.ansman.kotshi + +import com.squareup.kotlinpoet.TypeSpec +import com.squareup.kotlinpoet.metadata.ImmutableKmClass +import com.squareup.kotlinpoet.metadata.specs.ClassInspector +import com.squareup.kotlinpoet.metadata.specs.toTypeSpec +import com.squareup.kotlinpoet.metadata.toImmutableKmClass +import javax.lang.model.element.Element + +class MetadataAccessor(private val classInspector: ClassInspector) { + private val metadataPerType = mutableMapOf() + private val typeSpecPerType = mutableMapOf() + + fun getMetadata(type: Element): ImmutableKmClass = + metadataPerType.getOrPut(type) { + type.metadata.toImmutableKmClass() + } + + fun getTypeSpec(type: Element): TypeSpec = + typeSpecPerType.getOrPut(type) { + getMetadata(type).toTypeSpec(classInspector) + } +} \ No newline at end of file diff --git a/compiler/src/main/kotlin/se/ansman/kotshi/SealedClassSubtype.kt b/compiler/src/main/kotlin/se/ansman/kotshi/SealedClassSubtype.kt new file mode 100644 index 00000000..3bdd1962 --- /dev/null +++ b/compiler/src/main/kotlin/se/ansman/kotshi/SealedClassSubtype.kt @@ -0,0 +1,84 @@ +package se.ansman.kotshi + +import com.squareup.kotlinpoet.ClassName +import com.squareup.kotlinpoet.CodeBlock +import com.squareup.kotlinpoet.Dynamic +import com.squareup.kotlinpoet.LambdaTypeName +import com.squareup.kotlinpoet.ParameterizedTypeName +import com.squareup.kotlinpoet.TypeName +import com.squareup.kotlinpoet.TypeVariableName +import com.squareup.kotlinpoet.WildcardTypeName +import com.squareup.kotlinpoet.asClassName +import com.squareup.kotlinpoet.tag +import se.ansman.kotshi.generators.typesParameter +import java.lang.reflect.ParameterizedType +import javax.lang.model.element.TypeElement + +class SealedClassSubtype( + metadataAccessor: MetadataAccessor, + val type: TypeElement, + val label: String +) : TypeRenderer() { + val className = type.asClassName() + val typeSpec = metadataAccessor.getTypeSpec(type) + + override fun renderTypeVariable(typeVariable: TypeVariableName): CodeBlock { + val superParameters = (typeSpec.superclass as? ParameterizedTypeName) + ?.typeArguments + ?: emptyList() + + fun TypeName.findAccessor(typesIndex: Int): CodeBlock? { + return when (this) { + is ClassName, + Dynamic, + is LambdaTypeName -> null + is WildcardTypeName -> { + for (outType in outTypes) { + outType.findAccessor(typesIndex)?.let { return it } + } + for (inType in inTypes) { + inType.findAccessor(typesIndex)?.let { return it } + } + null + } + is TypeVariableName -> { + if (name.contentEquals(typeVariable.name)) { + CodeBlock.of("") + } else { + for (bound in bounds) { + bound.findAccessor(typesIndex)?.let { return it } + } + null + } + } + is ParameterizedTypeName -> { + typeArguments.forEachIndexed { index, typeName -> + val accessor = typeName.findAccessor(typesIndex) ?: return@forEachIndexed + return CodeBlock.builder() + .addControlFlow(".let") { + add("it as? %T\n", ParameterizedType::class.java) + indent() + add("?: throw %T(%P)\n", IllegalArgumentException::class.java, "The type \${${typesParameter.name}[$typesIndex]} is not a valid type constraint for the \$this") + unindent() + } + .add(".actualTypeArguments[%L]", index) + .add(accessor) + .build() + } + null + } + } + } + + superParameters.forEachIndexed { index, superParameter -> + val accessor = superParameter.findAccessor(index) ?: return@forEachIndexed + return CodeBlock.builder() + .add("%N[%L]\n", typesParameter, index) + .indent() + .add(accessor) + .unindent() + .build() + } + throw ProcessingError("Could not determine type variable type", typeVariable.tag() ?: type) + } +} \ No newline at end of file diff --git a/compiler/src/main/kotlin/se/ansman/kotshi/TypeRenderer.kt b/compiler/src/main/kotlin/se/ansman/kotshi/TypeRenderer.kt new file mode 100644 index 00000000..456d1d1e --- /dev/null +++ b/compiler/src/main/kotlin/se/ansman/kotshi/TypeRenderer.kt @@ -0,0 +1,114 @@ +/* + * 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 + * + * https://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 se.ansman.kotshi + +import com.squareup.kotlinpoet.ARRAY +import com.squareup.kotlinpoet.ClassName +import com.squareup.kotlinpoet.CodeBlock +import com.squareup.kotlinpoet.ParameterizedTypeName +import com.squareup.kotlinpoet.TypeName +import com.squareup.kotlinpoet.TypeVariableName +import com.squareup.kotlinpoet.WildcardTypeName +import com.squareup.moshi.Types + +/** + * Renders literals like `Types.newParameterizedType(List::class.java, String::class.java)`. + * Rendering is pluggable so that type variables can either be resolved or emitted as other code + * blocks. + */ +abstract class TypeRenderer { + abstract fun renderTypeVariable(typeVariable: TypeVariableName): CodeBlock + + fun render(typeName: TypeName, forceBox: Boolean = false): CodeBlock = + when { + typeName.annotations.isNotEmpty() -> render(typeName.copy(annotations = emptyList()), forceBox) + typeName.isNullable -> renderObjectType(typeName.copy(nullable = false)) + else -> when (typeName) { + is ClassName -> { + if (forceBox) { + renderObjectType(typeName) + } else { + CodeBlock.of("%T::class.java", typeName) + } + } + + is ParameterizedTypeName -> { + // If it's an Array type, we shortcut this to return Types.arrayOf() + if (typeName.rawType == ARRAY) { + CodeBlock.of( + "%T.arrayOf(%L)", + Types::class, + renderObjectType(typeName.typeArguments[0]) + ) + } else { + val builder = CodeBlock.builder().apply { + add("%T.", Types::class) + val enclosingClassName = typeName.rawType.enclosingClassName() + if (enclosingClassName != null) { + add("newParameterizedTypeWithOwner(%L, ", render(enclosingClassName)) + } else { + add("newParameterizedType(") + } + add("%T::class.java", typeName.rawType) + for (typeArgument in typeName.typeArguments) { + add(", %L", renderObjectType(typeArgument)) + } + add(")") + } + builder.build() + } + } + + is WildcardTypeName -> { + val target: TypeName + val method: String + when { + typeName.inTypes.size == 1 -> { + target = typeName.inTypes[0] + method = "supertypeOf" + } + typeName.outTypes.size == 1 -> { + target = typeName.outTypes[0] + method = "subtypeOf" + } + else -> throw IllegalArgumentException( + "Unrepresentable wildcard type. Cannot have more than one bound: $typeName" + ) + } + CodeBlock.of("%T.%L(%L)", Types::class, method, render(target, forceBox = true)) + } + + is TypeVariableName -> renderTypeVariable(typeName) + + else -> throw IllegalArgumentException("Unrepresentable type: $typeName") + } + } + + private fun renderObjectType(typeName: TypeName): CodeBlock = + if (typeName.isPrimitive) { + CodeBlock.of("%T::class.javaObjectType", typeName) + } else { + render(typeName) + } + + companion object { + operator fun invoke(renderer: (TypeVariableName) -> CodeBlock): TypeRenderer = + object : TypeRenderer() { + override fun renderTypeVariable(typeVariable: TypeVariableName): CodeBlock = renderer(typeVariable) + } + } +} \ No newline at end of file diff --git a/compiler/src/main/kotlin/se/ansman/kotshi/generators/AdapterGenerator.kt b/compiler/src/main/kotlin/se/ansman/kotshi/generators/AdapterGenerator.kt index 8d209ead..9cc5bb87 100644 --- a/compiler/src/main/kotlin/se/ansman/kotshi/generators/AdapterGenerator.kt +++ b/compiler/src/main/kotlin/se/ansman/kotshi/generators/AdapterGenerator.kt @@ -19,9 +19,7 @@ import com.squareup.kotlinpoet.metadata.isInner import com.squareup.kotlinpoet.metadata.isInternal import com.squareup.kotlinpoet.metadata.isLocal import com.squareup.kotlinpoet.metadata.isPublic -import com.squareup.kotlinpoet.metadata.specs.ClassInspector import com.squareup.kotlinpoet.metadata.specs.internal.ClassInspectorUtil -import com.squareup.kotlinpoet.metadata.specs.toTypeSpec import com.squareup.moshi.JsonAdapter import com.squareup.moshi.JsonDataException import com.squareup.moshi.JsonReader @@ -31,6 +29,7 @@ import se.ansman.kotshi.GeneratedAdapter import se.ansman.kotshi.JsonDefaultValue import se.ansman.kotshi.KotshiJsonAdapterFactory import se.ansman.kotshi.KotshiUtils +import se.ansman.kotshi.MetadataAccessor import se.ansman.kotshi.NamedJsonAdapter import se.ansman.kotshi.Polymorphic import se.ansman.kotshi.PolymorphicLabel @@ -41,7 +40,9 @@ import se.ansman.kotshi.applyIf import se.ansman.kotshi.maybeAddGeneratedAnnotation import se.ansman.kotshi.nullable import java.io.IOException +import java.lang.reflect.Type import javax.annotation.processing.Filer +import javax.annotation.processing.Messager import javax.lang.model.SourceVersion import javax.lang.model.element.TypeElement import javax.lang.model.type.TypeKind @@ -50,12 +51,13 @@ import javax.lang.model.util.Types @Suppress("UnstableApiUsage") abstract class AdapterGenerator( - classInspector: ClassInspector, + protected val metadataAccessor: MetadataAccessor, protected val types: Types, protected val elements: Elements, protected val element: TypeElement, protected val metadata: ImmutableKmClass, - protected val globalConfig: GlobalConfig + protected val globalConfig: GlobalConfig, + protected val messager: Messager ) { protected val nameAllocator = NameAllocator().apply { newName("options") @@ -66,7 +68,7 @@ abstract class AdapterGenerator( newName("it") } - protected val elementTypeSpec = metadata.toTypeSpec(classInspector) + protected val elementTypeSpec = metadataAccessor.getTypeSpec(element) protected val className = ClassInspectorUtil.createClassName(metadata.name) private val typeVariables = elementTypeSpec.typeVariables // Removes the variance @@ -93,7 +95,8 @@ abstract class AdapterGenerator( throw ProcessingError("Classes annotated with @JsonSerializable must public or internal", element) } - val adapterClassName = ClassName(className.packageName, "Kotshi${className.simpleNames.joinToString("_")}JsonAdapter") + val adapterClassName = + ClassName(className.packageName, "Kotshi${className.simpleNames.joinToString("_")}JsonAdapter") val typeSpec = TypeSpec.classBuilder(adapterClassName) .addModifiers(KModifier.INTERNAL) @@ -117,7 +120,11 @@ abstract class AdapterGenerator( typeVariables = typeVariables, requiresMoshi = typeSpec.primaryConstructor ?.parameters - ?.any { it.name == "moshi" } + ?.contains(moshiParameter) + ?: false, + requiresTypes = typeSpec.primaryConstructor + ?.parameters + ?.contains(typesParameter) ?: false ) } @@ -156,7 +163,7 @@ abstract class AdapterGenerator( MoreElements.asType(types.asElement(superclass)).getPolymorphicLabels(types, output) } val labelKey = nearestPolymorpic()?.labelKey - ?:return output + ?: return output val label = getAnnotation(PolymorphicLabel::class.java)?.value ?: return output output[labelKey] = label @@ -194,6 +201,7 @@ val jsonReader = JsonReader::class.java.asClassName() val writerParameter = ParameterSpec.builder("writer", jsonWriter).build() val readerParameter = ParameterSpec.builder("reader", jsonReader).build() val moshiParameter = ParameterSpec.builder("moshi", Moshi::class.java).build() +val typesParameter = ParameterSpec.builder("types", Array::class.plusParameter(Type::class)).build() data class GlobalConfig( val useAdaptersForPrimitives: Boolean, diff --git a/compiler/src/main/kotlin/se/ansman/kotshi/generators/DataClassAdapterGenerator.kt b/compiler/src/main/kotlin/se/ansman/kotshi/generators/DataClassAdapterGenerator.kt index 71e6e712..e0747ab6 100644 --- a/compiler/src/main/kotlin/se/ansman/kotshi/generators/DataClassAdapterGenerator.kt +++ b/compiler/src/main/kotlin/se/ansman/kotshi/generators/DataClassAdapterGenerator.kt @@ -3,27 +3,32 @@ package se.ansman.kotshi.generators 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.Dynamic import com.squareup.kotlinpoet.FLOAT import com.squareup.kotlinpoet.FunSpec import com.squareup.kotlinpoet.INT import com.squareup.kotlinpoet.KModifier import com.squareup.kotlinpoet.LONG -import com.squareup.kotlinpoet.ParameterSpec +import com.squareup.kotlinpoet.LambdaTypeName import com.squareup.kotlinpoet.ParameterizedTypeName import com.squareup.kotlinpoet.ParameterizedTypeName.Companion.plusParameter 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.asTypeName import com.squareup.kotlinpoet.jvm.throws import com.squareup.kotlinpoet.metadata.ImmutableKmClass import com.squareup.kotlinpoet.metadata.isData -import com.squareup.kotlinpoet.metadata.specs.ClassInspector import com.squareup.kotlinpoet.tag import se.ansman.kotshi.AdapterKey import se.ansman.kotshi.JsonSerializable +import se.ansman.kotshi.MetadataAccessor import se.ansman.kotshi.ProcessingError import se.ansman.kotshi.Property import se.ansman.kotshi.STRING @@ -43,7 +48,7 @@ import se.ansman.kotshi.isPrimitive import se.ansman.kotshi.notNull import se.ansman.kotshi.nullable import se.ansman.kotshi.suggestedAdapterName -import java.lang.reflect.Type +import javax.annotation.processing.Messager import javax.lang.model.element.AnnotationMirror import javax.lang.model.element.AnnotationValue import javax.lang.model.element.AnnotationValueVisitor @@ -54,13 +59,14 @@ import javax.lang.model.util.Elements import javax.lang.model.util.Types class DataClassAdapterGenerator( - classInspector: ClassInspector, + metadataAccessor: MetadataAccessor, types: Types, elements: Elements, element: TypeElement, metadata: ImmutableKmClass, - globalConfig: GlobalConfig -) : AdapterGenerator(classInspector, types, elements, element, metadata, globalConfig) { + globalConfig: GlobalConfig, + messager: Messager +) : AdapterGenerator(metadataAccessor, types, elements, element, metadata, globalConfig, messager) { init { require(metadata.isData) } @@ -95,7 +101,7 @@ class DataClassAdapterGenerator( .applyIf(adapterKeys.isNotEmpty()) { addParameter(moshiParameter) } - .applyIf(typeVariables.isNotEmpty()) { + .applyIf(adapterKeys.any { it.key.type.hasTypeVariable }) { addParameter(typesParameter) } .build()) @@ -409,141 +415,149 @@ class DataClassAdapterGenerator( ) } - private data class PropertyVariables( - val value: PropertySpec, - val helper: PropertySpec? - ) { - val isNotSet: CodeBlock by lazy(LazyThreadSafetyMode.NONE) { - if (helper == null) { - CodeBlock.of("%N == null", value) - } else { - CodeBlock.of("!%N", helper) - } +private data class PropertyVariables( + val value: PropertySpec, + val helper: PropertySpec? +) { + val isNotSet: CodeBlock by lazy(LazyThreadSafetyMode.NONE) { + if (helper == null) { + CodeBlock.of("%N == null", value) + } else { + CodeBlock.of("!%N", helper) } + } - val isSet: CodeBlock by lazy(LazyThreadSafetyMode.NONE) { - if (helper == null) { - CodeBlock.of("%N != null", value) - } else { - CodeBlock.of("%N", helper) - } + val isSet: CodeBlock by lazy(LazyThreadSafetyMode.NONE) { + if (helper == null) { + CodeBlock.of("%N != null", value) + } else { + CodeBlock.of("%N", helper) } } +} - @OptIn(ExperimentalStdlibApi::class) - private fun CodeBlock.Builder.add(value: AnnotationValue, valueType: TypeMirror): CodeBlock.Builder = apply { - value.accept(object : AnnotationValueVisitor { - override fun visitFloat(f: Float, p: Nothing?) { - add("${f}f") - } +@OptIn(ExperimentalStdlibApi::class) +private fun CodeBlock.Builder.add(value: AnnotationValue, valueType: TypeMirror): CodeBlock.Builder = apply { + value.accept(object : AnnotationValueVisitor { + override fun visitFloat(f: Float, p: Nothing?) { + add("${f}f") + } - override fun visitByte(b: Byte, p: Nothing?) { - add("(${b}).toByte()") - } + override fun visitByte(b: Byte, p: Nothing?) { + add("(${b}).toByte()") + } - override fun visitShort(s: Short, p: Nothing?) { - add("($s).toShort()") - } + override fun visitShort(s: Short, p: Nothing?) { + add("($s).toShort()") + } - override fun visitChar(c: Char, p: Nothing?) { - if (c == '\'') { - add("'\\''") - } else { - add("'$c'") - } + override fun visitChar(c: Char, p: Nothing?) { + if (c == '\'') { + add("'\\''") + } else { + add("'$c'") } + } - override fun visitUnknown(av: AnnotationValue?, p: Nothing?) = throw AssertionError() + override fun visitUnknown(av: AnnotationValue?, p: Nothing?) = throw AssertionError() - override fun visit(av: AnnotationValue?, p: Nothing?) = throw AssertionError() + override fun visit(av: AnnotationValue?, p: Nothing?) = throw AssertionError() - override fun visit(av: AnnotationValue?) =throw AssertionError() + override fun visit(av: AnnotationValue?) =throw AssertionError() - override fun visitArray(vals: List, p: Nothing?) { - val arrayCreator = when ((valueType.asTypeName() as ParameterizedTypeName).typeArguments.single()) { - BYTE -> "byteArrayOf" - CHAR -> "charArrayOf" - SHORT -> "shortArrayOf" - INT -> "intArrayOf" - LONG -> "longArrayOf" - FLOAT -> "floatArrayOf" - DOUBLE -> "doubleArrayOf" - BOOLEAN -> "booleanArrayOf" - else -> "arrayOf" - } + override fun visitArray(vals: List, p: Nothing?) { + val arrayCreator = when ((valueType.asTypeName() as ParameterizedTypeName).typeArguments.single()) { + BYTE -> "byteArrayOf" + CHAR -> "charArrayOf" + SHORT -> "shortArrayOf" + INT -> "intArrayOf" + LONG -> "longArrayOf" + FLOAT -> "floatArrayOf" + DOUBLE -> "doubleArrayOf" + BOOLEAN -> "booleanArrayOf" + else -> "arrayOf" + } - if (vals.isEmpty()) { - add("$arrayCreator()") - } else { - add("$arrayCreator(") - if (vals.size > 1) { - add("⇥\n") - } - vals.forEachIndexed { i, value -> - if (i > 0) { - add(",\n") - } - value.accept(this, null) - } - if (vals.size > 1) { - add("⇤\n") + if (vals.isEmpty()) { + add("$arrayCreator()") + } else { + add("$arrayCreator(") + if (vals.size > 1) { + add("⇥\n") + } + vals.forEachIndexed { i, value -> + if (i > 0) { + add(",\n") } - add(")") + value.accept(this, null) } + if (vals.size > 1) { + add("⇤\n") + } + add(")") } + } - override fun visitBoolean(b: Boolean, p: Nothing?) { - add(if (b) "true" else "false") - } + override fun visitBoolean(b: Boolean, p: Nothing?) { + add(if (b) "true" else "false") + } - override fun visitLong(i: Long, p: Nothing?) { - add("${i}L") - } + override fun visitLong(i: Long, p: Nothing?) { + add("${i}L") + } - override fun visitType(t: TypeMirror, p: Nothing?) { - add("%T::class.java", t.asTypeName()) - } + override fun visitType(t: TypeMirror, p: Nothing?) { + add("%T::class.java", t.asTypeName()) + } - override fun visitString(s: String, p: Nothing?) { - add("%S", s) - } + override fun visitString(s: String, p: Nothing?) { + add("%S", s) + } - override fun visitDouble(d: Double, p: Nothing?) { - val s = d.toString() - add(s) - if ('.' !in s) add(".0") - } + override fun visitDouble(d: Double, p: Nothing?) { + val s = d.toString() + add(s) + if ('.' !in s) add(".0") + } - override fun visitEnumConstant(c: VariableElement, p: Nothing?) { - add("%T.%N", c.asType().asTypeName(), c.simpleName) - } + override fun visitEnumConstant(c: VariableElement, p: Nothing?) { + add("%T.%N", c.asType().asTypeName(), c.simpleName) + } - override fun visitAnnotation(a: AnnotationMirror, p: Nothing?) { - add(a) - } + override fun visitAnnotation(a: AnnotationMirror, p: Nothing?) { + add(a) + } - override fun visitInt(i: Int, p: Nothing?) { - add("$i") - } - }, null) - } + override fun visitInt(i: Int, p: Nothing?) { + add("$i") + } + }, null) +} - private fun CodeBlock.Builder.add(annotation: AnnotationMirror): CodeBlock.Builder = apply { - if (annotation.elementValues.isEmpty()) { - add("%T::class.java.%M()", annotation.annotationType.asTypeName(), kotshiUtilsCreateJsonQualifierImplementation) - } else { - add("%T::class.java.%M(mapOf(⇥", annotation.annotationType.asTypeName(), kotshiUtilsCreateJsonQualifierImplementation) - annotation.elementValues.entries.forEachIndexed { i, (element, value) -> - if (i > 0) { - add(",") - } - add("\n") - add("%S·to·", element.simpleName) - add(value, element.returnType) - add("") +private fun CodeBlock.Builder.add(annotation: AnnotationMirror): CodeBlock.Builder = apply { + if (annotation.elementValues.isEmpty()) { + add("%T::class.java.%M()", annotation.annotationType.asTypeName(), kotshiUtilsCreateJsonQualifierImplementation) + } else { + add("%T::class.java.%M(mapOf(⇥", annotation.annotationType.asTypeName(), kotshiUtilsCreateJsonQualifierImplementation) + annotation.elementValues.entries.forEachIndexed { i, (element, value) -> + if (i > 0) { + add(",") } - add("⇤\n))") + add("\n") + add("%S·to·", element.simpleName) + add(value, element.returnType) + add("") } + add("⇤\n))") } +} -private val typesParameter = ParameterSpec.builder("types", Array::class.plusParameter(Type::class)).build() \ No newline at end of file +private val TypeName.hasTypeVariable: Boolean + get() = when (this) { + is ClassName -> false + Dynamic -> false + is LambdaTypeName -> receiver?.hasTypeVariable ?: false || parameters.any { it.type.hasTypeVariable } || returnType.hasTypeVariable + is ParameterizedTypeName -> typeArguments.any { it.hasTypeVariable } + is TypeVariableName -> true + is WildcardTypeName -> inTypes.any { it.hasTypeVariable } || outTypes.any { it.hasTypeVariable } + } \ No newline at end of file diff --git a/compiler/src/main/kotlin/se/ansman/kotshi/generators/EnumAdapterGenerator.kt b/compiler/src/main/kotlin/se/ansman/kotshi/generators/EnumAdapterGenerator.kt index 4b5226e3..6e3ace3a 100644 --- a/compiler/src/main/kotlin/se/ansman/kotshi/generators/EnumAdapterGenerator.kt +++ b/compiler/src/main/kotlin/se/ansman/kotshi/generators/EnumAdapterGenerator.kt @@ -6,25 +6,27 @@ import com.squareup.kotlinpoet.TypeSpec import com.squareup.kotlinpoet.jvm.throws import com.squareup.kotlinpoet.metadata.ImmutableKmClass import com.squareup.kotlinpoet.metadata.isEnum -import com.squareup.kotlinpoet.metadata.specs.ClassInspector +import se.ansman.kotshi.MetadataAccessor import se.ansman.kotshi.ProcessingError import se.ansman.kotshi.addControlFlow import se.ansman.kotshi.addNextControlFlow import se.ansman.kotshi.addWhen import se.ansman.kotshi.jsonName import se.ansman.kotshi.nullable +import javax.annotation.processing.Messager import javax.lang.model.element.TypeElement import javax.lang.model.util.Elements import javax.lang.model.util.Types class EnumAdapterGenerator( - classInspector: ClassInspector, + metadataAccessor: MetadataAccessor, types: Types, elements: Elements, element: TypeElement, metadata: ImmutableKmClass, - globalConfig: GlobalConfig -) : AdapterGenerator(classInspector, types, elements, element, metadata, globalConfig) { + globalConfig: GlobalConfig, + messager: Messager +) : AdapterGenerator(metadataAccessor, types, elements, element, metadata, globalConfig, messager) { init { require(metadata.isEnum) } diff --git a/compiler/src/main/kotlin/se/ansman/kotshi/generators/ObjectAdapterGenerator.kt b/compiler/src/main/kotlin/se/ansman/kotshi/generators/ObjectAdapterGenerator.kt index a38ada58..5a6e3e83 100644 --- a/compiler/src/main/kotlin/se/ansman/kotshi/generators/ObjectAdapterGenerator.kt +++ b/compiler/src/main/kotlin/se/ansman/kotshi/generators/ObjectAdapterGenerator.kt @@ -6,23 +6,25 @@ import com.squareup.kotlinpoet.TypeSpec import com.squareup.kotlinpoet.jvm.throws import com.squareup.kotlinpoet.metadata.ImmutableKmClass import com.squareup.kotlinpoet.metadata.isObject -import com.squareup.kotlinpoet.metadata.specs.ClassInspector +import se.ansman.kotshi.MetadataAccessor import se.ansman.kotshi.addControlFlow import se.ansman.kotshi.addElse import se.ansman.kotshi.addIfElse import se.ansman.kotshi.nullable +import javax.annotation.processing.Messager import javax.lang.model.element.TypeElement import javax.lang.model.util.Elements import javax.lang.model.util.Types class ObjectAdapterGenerator( - classInspector: ClassInspector, + metadataAccessor: MetadataAccessor, types: Types, elements: Elements, element: TypeElement, metadata: ImmutableKmClass, - globalConfig: GlobalConfig -) : AdapterGenerator(classInspector, types, elements, element, metadata, globalConfig) { + globalConfig: GlobalConfig, + messager: Messager +) : AdapterGenerator(metadataAccessor, types, elements, element, metadata, globalConfig, messager) { init { require(metadata.isObject) } diff --git a/compiler/src/main/kotlin/se/ansman/kotshi/generators/SealedClassAdapterGenerator.kt b/compiler/src/main/kotlin/se/ansman/kotshi/generators/SealedClassAdapterGenerator.kt index c6bbdab3..5c6b7dcf 100644 --- a/compiler/src/main/kotlin/se/ansman/kotshi/generators/SealedClassAdapterGenerator.kt +++ b/compiler/src/main/kotlin/se/ansman/kotshi/generators/SealedClassAdapterGenerator.kt @@ -5,6 +5,7 @@ import com.squareup.kotlinpoet.ARRAY import com.squareup.kotlinpoet.CodeBlock import com.squareup.kotlinpoet.FunSpec import com.squareup.kotlinpoet.KModifier +import com.squareup.kotlinpoet.ParameterizedTypeName.Companion.parameterizedBy import com.squareup.kotlinpoet.ParameterizedTypeName.Companion.plusParameter import com.squareup.kotlinpoet.PropertySpec import com.squareup.kotlinpoet.TypeName @@ -13,42 +14,40 @@ import com.squareup.kotlinpoet.asClassName import com.squareup.kotlinpoet.jvm.throws import com.squareup.kotlinpoet.metadata.ImmutableKmClass import com.squareup.kotlinpoet.metadata.isSealed -import com.squareup.kotlinpoet.metadata.specs.ClassInspector import com.squareup.kotlinpoet.metadata.toImmutableKmClass import se.ansman.kotshi.JsonDefaultValue import se.ansman.kotshi.JsonSerializable +import se.ansman.kotshi.MetadataAccessor import se.ansman.kotshi.Polymorphic import se.ansman.kotshi.PolymorphicLabel import se.ansman.kotshi.ProcessingError +import se.ansman.kotshi.SealedClassSubtype import se.ansman.kotshi.addControlFlow import se.ansman.kotshi.addElse import se.ansman.kotshi.addIf import se.ansman.kotshi.addIfElse import se.ansman.kotshi.addWhile import se.ansman.kotshi.applyEachIndexed +import se.ansman.kotshi.applyIf import se.ansman.kotshi.metadata import se.ansman.kotshi.nullable +import javax.annotation.processing.Messager import javax.lang.model.element.Modifier import javax.lang.model.element.TypeElement import javax.lang.model.util.Elements import javax.lang.model.util.Types class SealedClassAdapterGenerator( - classInspector: ClassInspector, + metadataAccessor: MetadataAccessor, types: Types, elements: Elements, element: TypeElement, metadata: ImmutableKmClass, - globalConfig: GlobalConfig -) : AdapterGenerator(classInspector, types, elements, element, metadata, globalConfig) { + globalConfig: GlobalConfig, + messager: Messager +) : AdapterGenerator(metadataAccessor, types, elements, element, metadata, globalConfig, messager) { init { require(metadata.isSealed) - if (metadata.typeParameters.isNotEmpty()) { - throw ProcessingError("Generic sealed classes are not supported yet", element) - } - if (metadata.sealedSubclasses.isEmpty()) { - throw ProcessingError("Sealed classes without implementations are not supported", element) - } nameAllocator.newName("peek") nameAllocator.newName("labelIndex") @@ -68,17 +67,15 @@ class SealedClassAdapterGenerator( override fun TypeSpec.Builder.addMethods() { val implementations = metadata.findSealedClassImplementations(className).toList() - val subtypes = implementations - .asSequence() - .mapNotNull { - Subtype( - type = it, - label = it.getAnnotation(PolymorphicLabel::class.java) - ?.value - ?: return@mapNotNull null - ) - } - .toList() + val subtypes = implementations.mapNotNull { + SealedClassSubtype( + metadataAccessor = metadataAccessor, + type = it, + label = it.getAnnotation(PolymorphicLabel::class.java) + ?.value + ?: return@mapNotNull null + ) + } if (subtypes.isEmpty()) { throw ProcessingError("No classes annotated with @PolymorphicLabel", element) @@ -107,18 +104,31 @@ class SealedClassAdapterGenerator( } val adapterType = jsonAdapter.plusParameter(typeName) - val adapters = PropertySpec.builder(nameAllocator.newName("adapters"), ARRAY.plusParameter(adapterType), KModifier.PRIVATE) - .initializer(CodeBlock.builder() - .add("arrayOf(«") - .applyEachIndexed(subtypes) { index, subtype -> - if (index > 0) { - add(",") + val adapters = + PropertySpec.builder(nameAllocator.newName("adapters"), ARRAY.plusParameter(adapterType), KModifier.PRIVATE) + .initializer(CodeBlock.builder() + .add("arrayOf(") + .indent() + .applyEachIndexed(subtypes) { index, subtype -> + if (index > 0) { + add(",") + } + add("\n%N.adapter<%T>(", moshiParameter, typeName) + + add(subtype.render( + typeName = if (subtype.typeSpec.typeVariables.isEmpty()) { + subtype.type.asClassName() + } else { + subtype.type.asClassName().parameterizedBy(subtype.typeSpec.typeVariables) + }, + forceBox = true + )) + add(")") } - add("\n%N.adapter<%T>(%T::class.java)", moshiParameter, typeName, subtype.className) - } - .add("»\n)\n") - .build()) - .build() + .unindent() + .add("\n)\n") + .build()) + .build() val defaultAdapter = if (defaultType == null) { null @@ -136,7 +146,12 @@ class SealedClassAdapterGenerator( } this - .primaryConstructor(FunSpec.constructorBuilder().addParameter(moshiParameter).build()) + .primaryConstructor(FunSpec.constructorBuilder() + .addParameter(moshiParameter) + .applyIf(typeVariables.isNotEmpty()) { + addParameter(typesParameter) + } + .build()) .addProperty(adapters) .addFunction(FunSpec.builder("toJson") .addModifiers(KModifier.OVERRIDE) @@ -149,7 +164,11 @@ class SealedClassAdapterGenerator( .addElse { addControlFlow("val adapter = when (%N)", value) { subtypes.forEachIndexed { index, subtype -> - addStatement("is %T·-> %N[%L]", subtype.className, adapters, index) + val generics = subtype.typeSpec.typeVariables.map { "*" } + .takeIf { it.isNotEmpty() } + ?.joinToString(", ", prefix = "<", postfix = ">") + ?: "" + addStatement("is %T%L·-> %N[%L]", subtype.className, generics, adapters, index) } if (defaultAdapter != null && defaultType != null && subtypes.none { it.type == defaultType }) { addStatement("is %T·-> %L", defaultType, defaultAdapter) @@ -163,7 +182,12 @@ class SealedClassAdapterGenerator( .throws(ioException) .addParameter(readerParameter) .returns(typeName.nullable()) - .addControlFlow("return·if·(%N.peek()·==·%T.NULL)", readerParameter, jsonReaderToken, close = false) { + .addControlFlow( + "return·if·(%N.peek()·==·%T.NULL)", + readerParameter, + jsonReaderToken, + close = false + ) { addStatement("%N.nextNull()", readerParameter) } .addElse { @@ -179,7 +203,11 @@ class SealedClassAdapterGenerator( addStatement("val·labelIndex·= peek.selectString(options)") addControlFlow("val·adapter·= if·(labelIndex·==·-1)", close = false) { if (annotation.onInvalid == Polymorphic.Fallback.FAIL || defaultType == null && annotation.onInvalid == Polymorphic.Fallback.DEFAULT) { - addStatement("throw·%T(%S·+ peek.nextString())", jsonDataException, "Expected one of $labels for key '$labelKey' but found ") + addStatement( + "throw·%T(%S·+ peek.nextString())", + jsonDataException, + "Expected one of $labels for key '$labelKey' but found " + ) } else if (annotation.onInvalid == Polymorphic.Fallback.NULL) { addStatement("%N.skipValue()", readerParameter) addStatement("return·null") @@ -199,7 +227,11 @@ class SealedClassAdapterGenerator( addStatement("%N.skipValue()", readerParameter) addStatement("null") } else { - addStatement("%L.fromJson(%N)", defaultAdapter ?: throw AssertionError("Unhandled case"), readerParameter) + addStatement( + "%L.fromJson(%N)", + defaultAdapter ?: throw AssertionError("Unhandled case"), + readerParameter + ) } } } @@ -221,9 +253,6 @@ class SealedClassAdapterGenerator( if (it.getAnnotation(JsonSerializable::class.java) == null) { throw ProcessingError("All subclasses of a sealed class must be @JsonSerializable", it) } - if (it.typeParameters.isNotEmpty()) { - throw ProcessingError("Generic sealed class implementations are not supported", it) - } } .flatMap { if (Modifier.ABSTRACT in it.modifiers) { @@ -237,27 +266,30 @@ class SealedClassAdapterGenerator( when { polymorphic.labelKey == labelKey -> { if (polymorphicLabel != null) { - throw ProcessingError("Children of a sealed class with the same label key must not be annotated with @PolymorphicLabel", it) + throw ProcessingError( + "Children of a sealed class with the same label key must not be annotated with @PolymorphicLabel", + it + ) } kmClass.findSealedClassImplementations(it.asClassName()) } polymorphicLabel == null -> { - throw ProcessingError("Children of a sealed class with a different label key must be annotated with @PolymorphicLabel", it) + throw ProcessingError( + "Children of a sealed class with a different label key must be annotated with @PolymorphicLabel", + it + ) } else -> sequenceOf(it) } } else { if (it.getAnnotation(PolymorphicLabel::class.java) == null && it.getAnnotation(JsonDefaultValue::class.java) == null) { - throw ProcessingError("Subclasses of sealed classes must be annotated with @PolymorphicLabel or @JsonDefaultValue", it) + throw ProcessingError( + "Subclasses of sealed classes must be annotated with @PolymorphicLabel or @JsonDefaultValue", + it + ) } sequenceOf(it) } } - private data class Subtype( - val type: TypeElement, - val label: String - ) { - val className = type.asClassName() - } } \ No newline at end of file diff --git a/tests/src/main/kotlin/se/ansman/kotshi/SealedClassWithComplexGeneric.kt b/tests/src/main/kotlin/se/ansman/kotshi/SealedClassWithComplexGeneric.kt new file mode 100644 index 00000000..90bf52ac --- /dev/null +++ b/tests/src/main/kotlin/se/ansman/kotshi/SealedClassWithComplexGeneric.kt @@ -0,0 +1,13 @@ +package se.ansman.kotshi + +@JsonSerializable +@Polymorphic(labelKey = "type") +sealed class SealedClassWithComplexGeneric { + @JsonSerializable + @PolymorphicLabel("type1") + data class Type1(val a: List, val b: B) : SealedClassWithComplexGeneric>() + + @JsonSerializable + @PolymorphicLabel("type2") + data class Type2(val error: String) : SealedClassWithComplexGeneric() +} \ No newline at end of file diff --git a/tests/src/main/kotlin/se/ansman/kotshi/SealedClassWithGeneric.kt b/tests/src/main/kotlin/se/ansman/kotshi/SealedClassWithGeneric.kt new file mode 100644 index 00000000..c47a345d --- /dev/null +++ b/tests/src/main/kotlin/se/ansman/kotshi/SealedClassWithGeneric.kt @@ -0,0 +1,13 @@ +package se.ansman.kotshi + +@JsonSerializable +@Polymorphic(labelKey = "type") +sealed class SealedClassWithGeneric { + @JsonSerializable + @PolymorphicLabel("success") + data class Success(val data: T) : SealedClassWithGeneric() + + @JsonSerializable + @PolymorphicLabel("error") + data class Error(val error: String) : SealedClassWithGeneric() +} \ No newline at end of file diff --git a/tests/src/test/kotlin/se/ansman/kotshi/SealedClassWithComplexGenericTest.kt b/tests/src/test/kotlin/se/ansman/kotshi/SealedClassWithComplexGenericTest.kt new file mode 100644 index 00000000..0eee902e --- /dev/null +++ b/tests/src/test/kotlin/se/ansman/kotshi/SealedClassWithComplexGenericTest.kt @@ -0,0 +1,37 @@ +package se.ansman.kotshi + +import com.google.common.truth.Truth.assertThat +import com.squareup.moshi.Moshi +import com.squareup.moshi.Types +import org.junit.Test + +class SealedClassWithComplexGenericTest { + private val adapter = Moshi.Builder() + .add(TestFactory) + .build() + .adapter>>( + Types.newParameterizedType( + SealedClassWithComplexGeneric::class.java, + String::class.java, + Types.newParameterizedType(List::class.java, Int::class.javaObjectType) + ) + ) + + @Test + fun testFromJson() { + assertThat(adapter.fromJson("""{"type":"type1","a":[1,2,3,4],"b":"hello"}""")) + .isEqualTo(SealedClassWithComplexGeneric.Type1(listOf(1, 2, 3, 4), "hello")) + + assertThat(adapter.fromJson("""{"type":"type2","error":"err"}""")) + .isEqualTo(SealedClassWithComplexGeneric.Type2>("err")) + } + + @Test + fun testToJson() { + assertThat(adapter.toJson(SealedClassWithComplexGeneric.Type1(listOf(1, 2, 3, 4), "hello"))) + .isEqualTo("""{"type":"type1","a":[1,2,3,4],"b":"hello"}""") + + assertThat(adapter.toJson(SealedClassWithComplexGeneric.Type2("err"))) + .isEqualTo("""{"type":"type2","error":"err"}""") + } +} \ No newline at end of file diff --git a/tests/src/test/kotlin/se/ansman/kotshi/SealedClassWithGenericTest.kt b/tests/src/test/kotlin/se/ansman/kotshi/SealedClassWithGenericTest.kt new file mode 100644 index 00000000..85a53439 --- /dev/null +++ b/tests/src/test/kotlin/se/ansman/kotshi/SealedClassWithGenericTest.kt @@ -0,0 +1,36 @@ +package se.ansman.kotshi + +import com.google.common.truth.Truth.assertThat +import com.squareup.moshi.Moshi +import com.squareup.moshi.Types +import org.junit.Test + +class SealedClassWithGenericTest { + private val adapter = Moshi.Builder() + .add(TestFactory) + .build() + .adapter>( + Types.newParameterizedType( + SealedClassWithGeneric::class.java, + String::class.java + ) + ) + + @Test + fun testFromJson() { + assertThat(adapter.fromJson("""{"type":"success","data":"hello"}""")) + .isEqualTo(SealedClassWithGeneric.Success("hello")) + + assertThat(adapter.fromJson("""{"type":"error","error":"Something went wrong"}""")) + .isEqualTo(SealedClassWithGeneric.Error("Something went wrong")) + } + + @Test + fun testToJson() { + assertThat(adapter.toJson(SealedClassWithGeneric.Success("hello"))) + .isEqualTo("""{"type":"success","data":"hello"}""") + + assertThat(adapter.toJson(SealedClassWithGeneric.Error("Something went wrong"))) + .isEqualTo("""{"type":"error","error":"Something went wrong"}""") + } +} \ No newline at end of file