Skip to content

Commit

Permalink
Add support for readOnly/writeOnly adapters for codeGen (kapt/KSP).
Browse files Browse the repository at this point in the history
  • Loading branch information
Tolriq committed Mar 24, 2024
1 parent 217a46c commit dc38561
Show file tree
Hide file tree
Showing 8 changed files with 147 additions and 11 deletions.
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -28,3 +28,4 @@ obj

# Temporary until generating a docsite
docs/
/local.properties
2 changes: 1 addition & 1 deletion gradle/libs.versions.toml
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
[versions]
autoService = "1.1.1"
jvmTarget = "1.8"
kotlin = "1.9.21"
kotlin = "1.9.23"
kotlinCompileTesting = "0.4.0"
kotlinpoet = "1.14.2"
ksp = "1.9.23-1.0.19"
Expand Down
6 changes: 6 additions & 0 deletions jitpack.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
jdk:
- openjdk20
before_install:
- sdk install java 17.0.1-open
- sdk install java 20.0.2-open
- sdk use java 20.0.2-open
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,8 @@ private const val TO_STRING_SIZE_BASE = TO_STRING_PREFIX.length + 1 // 1 is the
public class AdapterGenerator(
private val target: TargetType,
private val propertyList: List<PropertyGenerator>,
private val readOnly: Boolean,
private val writeOnly: Boolean,
) {

private companion object {
Expand Down Expand Up @@ -273,8 +275,9 @@ public class AdapterGenerator(
return CodeBlock.of("%N[%L]", typesParam, index)
}
}

result.addProperty(optionsProperty)
if (!writeOnly) {
result.addProperty(optionsProperty)
}
for (uniqueAdapter in nonTransientProperties.distinctBy { it.delegateKey }) {
result.addProperty(
uniqueAdapter.delegateKey.generateProperty(
Expand All @@ -287,8 +290,8 @@ public class AdapterGenerator(
}

result.addFunction(generateToStringFun())
result.addFunction(generateFromJsonFun(result))
result.addFunction(generateToJsonFun())
result.addFunction(generateFromJsonFun(result, writeOnly))
result.addFunction(generateToJsonFun(readOnly))

return result.build()
}
Expand Down Expand Up @@ -321,12 +324,27 @@ public class AdapterGenerator(
.build()
}

private fun generateFromJsonFun(classBuilder: TypeSpec.Builder): FunSpec {
private fun generateFromJsonFun(classBuilder: TypeSpec.Builder, writeOnly: Boolean): FunSpec {
val result = FunSpec.builder("fromJson")
.addModifiers(KModifier.OVERRIDE)
.addParameter(readerParam)
.returns(originalTypeName)

if (writeOnly) {
val name = originalRawTypeName.simpleNames.joinToString(".")
result.addStatement(
"throw·%T(%M(%L)·{ append(%S).append(%S).append('%L').append(%S) })",
UnsupportedOperationException::class,
MemberName("kotlin.text", "buildString"),
TO_STRING_SIZE_BASE + name.length + 53,
TO_STRING_PREFIX,
name,
")",
" is write only. @JsonClass is set with writeOnly=true",
)
return result.build()
}

for (property in nonTransientProperties) {
result.addCode("%L", property.generateLocalProperty())
if (property.hasLocalIsPresentName) {
Expand Down Expand Up @@ -677,12 +695,27 @@ public class AdapterGenerator(
)
}

private fun generateToJsonFun(): FunSpec {
private fun generateToJsonFun(readOnly: Boolean): FunSpec {
val result = FunSpec.builder("toJson")
.addModifiers(KModifier.OVERRIDE)
.addParameter(writerParam)
.addParameter(valueParam)

if (readOnly) {
val name = originalRawTypeName.simpleNames.joinToString(".")
result.addStatement(
"throw·%T(%M(%L)·{ append(%S).append(%S).append('%L').append(%S) })",
UnsupportedOperationException::class,
MemberName("kotlin.text", "buildString"),
TO_STRING_SIZE_BASE + name.length + 51,
TO_STRING_PREFIX,
name,
")",
" is read only. @JsonClass is set with readOnly=true",
)
return result.build()
}

result.beginControlFlow("if (%N == null)", valueParam)
result.addStatement(
"throw·%T(%S)",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -104,7 +104,15 @@ public class JsonClassCodegenProcessor : AbstractProcessor() {
}
val jsonClass = type.getAnnotation(annotation)
if (jsonClass.generateAdapter && jsonClass.generator.isEmpty()) {
val generator = adapterGenerator(type, cachedClassInspector) ?: continue
if (jsonClass.readOnly && jsonClass.writeOnly) {
messager.printMessage(
Diagnostic.Kind.ERROR,
"@JsonClass can't be both readOnly and writeOnly",
type,
)
continue
}
val generator = adapterGenerator(type, cachedClassInspector, jsonClass.readOnly, jsonClass.writeOnly) ?: continue
val preparedAdapter = generator
.prepare(generateProguardRules) { spec ->
spec.toBuilder()
Expand Down Expand Up @@ -137,6 +145,8 @@ public class JsonClassCodegenProcessor : AbstractProcessor() {
private fun adapterGenerator(
element: TypeElement,
cachedClassInspector: MoshiCachedClassInspector,
readOnly: Boolean,
writeOnly: Boolean,
): AdapterGenerator? {
val type = targetType(
messager,
Expand Down Expand Up @@ -174,7 +184,7 @@ public class JsonClassCodegenProcessor : AbstractProcessor() {
}
}

return AdapterGenerator(type, sortedProperties)
return AdapterGenerator(type, sortedProperties, readOnly, writeOnly)
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -89,7 +89,7 @@ private class JsonClassSymbolProcessor(

try {
val originatingFile = type.containingFile!!
val adapterGenerator = adapterGenerator(logger, resolver, type) ?: return emptyList()
val adapterGenerator = adapterGenerator(logger, resolver, type, jsonClassAnnotation.readOnly, jsonClassAnnotation.writeOnly) ?: return emptyList()
val preparedAdapter = adapterGenerator
.prepare(generateProguardRules) { spec ->
spec.toBuilder()
Expand All @@ -114,6 +114,8 @@ private class JsonClassSymbolProcessor(
logger: KSPLogger,
resolver: Resolver,
originalType: KSDeclaration,
readOnly: Boolean,
writeOnly: Boolean,
): AdapterGenerator? {
val type = targetType(originalType, resolver, logger) ?: return null

Expand Down Expand Up @@ -142,7 +144,7 @@ private class JsonClassSymbolProcessor(
}
}

return AdapterGenerator(type, sortedProperties)
return AdapterGenerator(type, sortedProperties, readOnly, writeOnly)
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -537,6 +537,75 @@ class JsonClassCodegenProcessorTest {
)
}

@Test
fun `ReadOnly adapter should throw on read`() {
val result = compile(
kotlin(
"source.kt",
"""
import com.squareup.moshi.JsonClass
typealias FirstName = String
typealias LastName = String
@JsonClass(generateAdapter = true, readOnly = true)
data class Person(val firstName: FirstName, val lastName: LastName, val hairColor: String)
""",
),
)
assertThat(result.exitCode).isEqualTo(KotlinCompilation.ExitCode.OK)

result.generatedFiles.filter { it.name == "PersonJsonAdapter.kt" }.forEach { generatedFile ->
assertThat(generatedFile.readText()).contains(
"""
throw UnsupportedOperationException(buildString(79) {
append("GeneratedJsonAdapter(").append("Person").append(')').append(" is read only. @JsonClass is set with readOnly=true")
})""",
)
}
}

@Test
fun `WriteOnly adapter should throw on write`() {
val result = compile(
kotlin(
"source.kt",
"""
import com.squareup.moshi.JsonClass
typealias FirstName = String
typealias LastName = String
@JsonClass(generateAdapter = true, writeOnly = true)
data class Person(val firstName: FirstName, val lastName: LastName, val hairColor: String)
""",
),
)
assertThat(result.exitCode).isEqualTo(KotlinCompilation.ExitCode.OK)
result.generatedFiles.filter { it.name == "PersonJsonAdapter.kt" }.forEach { generatedFile ->
assertThat(generatedFile.readText()).contains(
"""
throw UnsupportedOperationException(buildString(81) {
append("GeneratedJsonAdapter(").append("Person").append(')').append(" is write only. @JsonClass is set with writeOnly=true")
})""",
)
}
}

@Test
fun `Adapter can't be readOnly and writeOnly`() {
val result = compile(
kotlin(
"source.kt",
"""
import com.squareup.moshi.JsonClass
typealias FirstName = String
typealias LastName = String
@JsonClass(generateAdapter = true, writeOnly = true, readOnly = true)
data class Person(val firstName: FirstName, val lastName: LastName, val hairColor: String)
""",
),
)
assertThat(result.exitCode).isEqualTo(KotlinCompilation.ExitCode.COMPILATION_ERROR)
assertThat(result.messages).contains("@JsonClass can't be both readOnly and writeOnly")
}

@Test
fun `Processor should generate comprehensive proguard rules`() {
val result = compile(
Expand Down
15 changes: 15 additions & 0 deletions moshi/src/main/java/com/squareup/moshi/JsonClass.kt
Original file line number Diff line number Diff line change
Expand Up @@ -64,4 +64,19 @@ public annotation class JsonClass(
* expected signature.
*/
val generator: String = "",
/**
* An optional parameter that replace generated toJson() code of the adapter to simply throw a
* {@link JsonDataException}.
*
* <p>Reduce generated code size when data is never serialized to JSON.
*/
val readOnly: Boolean = false,

/**
* An optional parameter that replace generated fromJson() code of the adapter to simply throw a
* {@link JsonDataException}.
*
* <p>Reduce generated code size when data is never deserialized from JSON.
*/
val writeOnly: Boolean = false,
)

0 comments on commit dc38561

Please sign in to comment.