Add @Json.ignore (#1417)

* Default Json.name to an unset value

* Promote shared transient tests to DualKotlinTest

* Add new ignore property to Json

* Support it in ClassJsonAdapter

* Mention no enum/record support

* Support in KotlinJsonAdapter

* Rework code gen API to know of "ignored"

* Support in apt code gen

* Support in KSP

* Update old non-working transient example test

* Synthetic holders

* Use field on both
This commit is contained in:
Zac Sweers
2021-11-08 11:16:57 -05:00
committed by GitHub
parent 48e6dd3f03
commit 954ca46b9e
19 changed files with 317 additions and 108 deletions

View File

@@ -26,5 +26,6 @@ public data class TargetParameter(
val type: TypeName,
val hasDefault: Boolean,
val jsonName: String? = null,
val jsonIgnore: Boolean = false,
val qualifiers: Set<AnnotationSpec>? = null
)

View File

@@ -25,7 +25,8 @@ public data class TargetProperty(
val propertySpec: PropertySpec,
val parameter: TargetParameter?,
val visibility: KModifier,
val jsonName: String?
val jsonName: String?,
val jsonIgnore: Boolean
) {
val name: String get() = propertySpec.name
val type: TypeName get() = propertySpec.type

View File

@@ -64,6 +64,7 @@ import javax.tools.Diagnostic.Kind.WARNING
private val JSON_QUALIFIER = JsonQualifier::class.java
private val JSON = Json::class.asClassName()
private val TRANSIENT = Transient::class.asClassName()
private val OBJECT_CLASS = ClassName("java.lang", "Object")
private val VISIBILITY_MODIFIERS = setOf(
KModifier.INTERNAL,
@@ -95,7 +96,8 @@ internal fun primaryConstructor(
type = parameter.type,
hasDefault = parameter.defaultValue != null,
qualifiers = parameter.annotations.qualifiers(messager, elements),
jsonName = parameter.annotations.jsonName()
jsonName = parameter.annotations.jsonName(),
jsonIgnore = parameter.annotations.jsonIgnore(),
)
}
@@ -386,7 +388,6 @@ private fun declaredProperties(
currentClass: ClassName,
resolvedTypes: List<ResolvedTypeMapping>
): Map<String, TargetProperty> {
val result = mutableMapOf<String, TargetProperty>()
for (initialProperty in kotlinApi.propertySpecs) {
val resolvedType = resolveTypeArgs(
@@ -398,19 +399,22 @@ private fun declaredProperties(
val property = initialProperty.toBuilder(type = resolvedType).build()
val name = property.name
val parameter = constructor.parameters[name]
val isIgnored = property.annotations.any { it.typeName == TRANSIENT } ||
parameter?.jsonIgnore == true ||
property.annotations.jsonIgnore()
result[name] = TargetProperty(
propertySpec = property,
parameter = parameter,
visibility = property.modifiers.visibility(),
jsonName = parameter?.jsonName ?: property.annotations.jsonName()
?: name.escapeDollarSigns()
?: name.escapeDollarSigns(),
jsonIgnore = isIgnored
)
}
return result
}
private val TargetProperty.isTransient get() = propertySpec.annotations.any { it.typeName == Transient::class.asClassName() }
private val TargetProperty.isSettable get() = propertySpec.mutable || parameter != null
private val TargetProperty.isVisible: Boolean
get() {
@@ -429,11 +433,11 @@ internal fun TargetProperty.generator(
elements: Elements,
instantiateAnnotations: Boolean
): PropertyGenerator? {
if (isTransient) {
if (jsonIgnore) {
if (!hasDefault) {
messager.printMessage(
ERROR,
"No default value for transient property $name",
"No default value for transient/ignored property $name",
sourceElement
)
return null
@@ -510,16 +514,36 @@ private fun List<AnnotationSpec>?.qualifiers(
private fun List<AnnotationSpec>?.jsonName(): String? {
if (this == null) return null
return find { it.typeName == JSON }?.let { annotation ->
val mirror = requireNotNull(annotation.tag<AnnotationMirror>()) {
"Could not get the annotation mirror from the annotation spec"
}
mirror.elementValues.entries.single {
it.key.simpleName.contentEquals("name")
}.value.value as String
return filter { it.typeName == JSON }.firstNotNullOfOrNull { annotation ->
annotation.jsonName()
}
}
private fun List<AnnotationSpec>?.jsonIgnore(): Boolean {
if (this == null) return false
return filter { it.typeName == JSON }.firstNotNullOfOrNull { annotation ->
annotation.jsonIgnore()
} ?: false
}
private fun AnnotationSpec.jsonName(): String? {
return elementValue<String>("name").takeUnless { it == Json.UNSET_NAME }
}
private fun AnnotationSpec.jsonIgnore(): Boolean {
return elementValue<Boolean>("ignore") ?: false
}
private fun <T> AnnotationSpec.elementValue(name: String): T? {
val mirror = requireNotNull(tag<AnnotationMirror>()) {
"Could not get the annotation mirror from the annotation spec"
}
@Suppress("UNCHECKED_CAST")
return mirror.elementValues.entries.firstOrNull {
it.key.simpleName.contentEquals(name)
}?.value?.value as? T
}
private fun String.escapeDollarSigns(): String {
return replace("\$", "\${\'\$\'}")
}

View File

@@ -21,14 +21,12 @@ import com.google.devtools.ksp.symbol.KSClassDeclaration
import com.google.devtools.ksp.symbol.KSDeclaration
import com.squareup.kotlinpoet.AnnotationSpec
import com.squareup.kotlinpoet.KModifier
import com.squareup.kotlinpoet.asClassName
import com.squareup.moshi.JsonQualifier
import com.squareup.moshi.kotlin.codegen.api.DelegateKey
import com.squareup.moshi.kotlin.codegen.api.PropertyGenerator
import com.squareup.moshi.kotlin.codegen.api.TargetProperty
import com.squareup.moshi.kotlin.codegen.api.rawType
private val TargetProperty.isTransient get() = propertySpec.annotations.any { it.typeName == Transient::class.asClassName() }
private val TargetProperty.isSettable get() = propertySpec.mutable || parameter != null
private val TargetProperty.isVisible: Boolean
get() {
@@ -47,10 +45,10 @@ internal fun TargetProperty.generator(
originalType: KSDeclaration,
instantiateAnnotations: Boolean
): PropertyGenerator? {
if (isTransient) {
if (jsonIgnore) {
if (!hasDefault) {
logger.error(
"No default value for transient property $name",
"No default value for transient/ignored property $name",
originalType
)
return null

View File

@@ -213,7 +213,11 @@ private fun KSAnnotated?.qualifiers(resolver: Resolver): Set<AnnotationSpec> {
}
private fun KSAnnotated?.jsonName(): String? {
return this?.findAnnotationWithType<Json>()?.name
return this?.findAnnotationWithType<Json>()?.name?.takeUnless { it == Json.UNSET_NAME }
}
private fun KSAnnotated?.jsonIgnore(): Boolean {
return this?.findAnnotationWithType<Json>()?.ignore ?: false
}
private fun declaredProperties(
@@ -223,7 +227,6 @@ private fun declaredProperties(
resolver: Resolver,
typeParameterResolver: TypeParameterResolver,
): Map<String, TargetProperty> {
val result = mutableMapOf<String, TargetProperty>()
for (property in classDecl.getDeclaredProperties()) {
val initialType = property.type.resolve()
@@ -235,12 +238,14 @@ private fun declaredProperties(
val propertySpec = property.toPropertySpec(resolver, resolvedType, typeParameterResolver)
val name = propertySpec.name
val parameter = constructor.parameters[name]
val isTransient = property.isAnnotationPresent(Transient::class)
result[name] = TargetProperty(
propertySpec = propertySpec,
parameter = parameter,
visibility = property.getVisibility().toKModifier() ?: KModifier.PUBLIC,
jsonName = parameter?.jsonName ?: property.jsonName()
?: name.escapeDollarSigns()
?: name.escapeDollarSigns(),
jsonIgnore = isTransient || parameter?.jsonIgnore == true || property.jsonIgnore()
)
}

View File

@@ -334,7 +334,27 @@ class JsonClassCodegenProcessorTest(
)
assertThat(result.exitCode).isEqualTo(KotlinCompilation.ExitCode.COMPILATION_ERROR)
assertThat(result.messages).contains(
"error: No default value for transient property a"
"error: No default value for transient/ignored property a"
)
}
@Test
fun requiredIgnoredConstructorParameterFails() {
val result = compile(
kotlin(
"source.kt",
"""
import com.squareup.moshi.Json
import com.squareup.moshi.JsonClass
@JsonClass(generateAdapter = true)
class RequiredIgnoredConstructorParameter(@Json(ignore = true) var a: Int)
"""
)
)
assertThat(result.exitCode).isEqualTo(KotlinCompilation.ExitCode.COMPILATION_ERROR)
assertThat(result.messages).contains(
"error: No default value for transient/ignored property a"
)
}

View File

@@ -357,7 +357,28 @@ class JsonClassSymbolProcessorTest(
)
assertThat(result.exitCode).isEqualTo(KotlinCompilation.ExitCode.COMPILATION_ERROR)
assertThat(result.messages).contains(
"No default value for transient property a"
"No default value for transient/ignored property a"
)
}
@Test
fun requiredIgnoredConstructorParameterFails() {
val result = compile(
kotlin(
"source.kt",
"""
package test
import com.squareup.moshi.Json
import com.squareup.moshi.JsonClass
@JsonClass(generateAdapter = true)
class RequiredTransientConstructorParameter(@Json(ignore = true) var a: Int)
"""
)
)
assertThat(result.exitCode).isEqualTo(KotlinCompilation.ExitCode.COMPILATION_ERROR)
assertThat(result.messages).contains(
"No default value for transient/ignored property a"
)
}