Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
37 changes: 18 additions & 19 deletions GeneratorScripts/src/main/kotlin/autogenscripts/GenTypes.kt
Original file line number Diff line number Diff line change
Expand Up @@ -75,18 +75,15 @@ val fileFieldEnumOverrides = mapOf(
// ============================================================

// Sealed interfaces that are not generated by dukat but must exist in the datatypes package.
// Maps the interface name to its @JsonClassDiscriminator field, and whether it needs @Serializable.
data class SealedInterface(val discriminator: String, val serializable: Boolean = false)

// Interfaces that are deserialized polymorphically (as response fields) must be @Serializable,
// otherwise the class discriminator is not resolved at runtime.
// TODO refactor
// Maps the interface name to its @JsonClassDiscriminator field.
// They are always generated as @Serializable, otherwise the class discriminator
// is not resolved when they are deserialized polymorphically (as response fields).
val sealedInterfaces = mapOf(
"PostOrCommentOrPrivateMessage" to SealedInterface("type_", serializable = true),
"ResolveObjectView" to SealedInterface("type_", serializable = true),
"ReportCombinedView" to SealedInterface("type_", serializable = true),
"NotificationData" to SealedInterface("type_", serializable = true),
"PostCommentCombinedView" to SealedInterface("type_", serializable = true),
"PostOrCommentOrPrivateMessage" to "type_",
"ResolveObjectView" to "type_",
"ReportCombinedView" to "type_",
"NotificationData" to "type_",
"PostCommentCombinedView" to "type_",
)

// Which data classes implement which sealed interfaces and what @SerialName they carry.
Expand Down Expand Up @@ -218,19 +215,18 @@ fun addSealedInterfaceImpl(content: String, fileName: String, imports: MutableSe

/** Generates a sealed interface .kt file for each entry in sealedInterfaces. */
fun writeSealedInterfaces(datatypesDir: File, packageName: String) {
for ((name, info) in sealedInterfaces) {
val serializableImport = if (info.serializable) "\nimport kotlinx.serialization.Serializable" else ""
val serializableAnnotation = if (info.serializable) "\n@Serializable" else ""

for ((name, discriminator) in sealedInterfaces) {
File(datatypesDir, "$name.kt").writeText(
"""
package $packageName

import kotlinx.serialization.ExperimentalSerializationApi$serializableImport
import kotlinx.serialization.ExperimentalSerializationApi
import kotlinx.serialization.Serializable
import kotlinx.serialization.json.JsonClassDiscriminator

@OptIn(ExperimentalSerializationApi::class)$serializableAnnotation
@JsonClassDiscriminator("${info.discriminator}")
@OptIn(ExperimentalSerializationApi::class)
@Serializable
@JsonClassDiscriminator("$discriminator")
internal sealed interface $name
""".trimIndent() + "\n"
)
Expand Down Expand Up @@ -265,7 +261,8 @@ fun processDukatFile(typeFile: File, fileName: String, packageName: String, root
if (!body.endsWith("\n")) appendLine()
})

typeFile.delete()
// Older dukat versions emit `.d.kt` files, newer ones emit `.kt`, which is overwritten above.
if (typeFile.canonicalPath != outputFile.canonicalPath) typeFile.delete()
}

// ============================================================
Expand Down Expand Up @@ -311,8 +308,10 @@ fun downloadTypes(
println("Starting Dukat process...")
val proc = ProcessBuilder("bash", "-ic", "dukat temp/*.ts", "exit")
.directory(datatypesDir)
.redirectErrorStream(true)
.start()
println(String(proc.inputStream.readAllBytes()))
check(proc.waitFor() == 0) { "Dukat failed" }

// Process each dukat-generated .d.kt → clean .kt
datatypesDir.listFiles()!!.forEach { typeFile ->
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,11 @@ package it.vercruysse.lemmyapi.datatypes
import it.vercruysse.lemmyapi.CommonParcelize
import it.vercruysse.lemmyapi.DatatypeRoot
import kotlinx.serialization.ExperimentalSerializationApi
import kotlinx.serialization.Serializable
import kotlinx.serialization.json.JsonClassDiscriminator

@CommonParcelize
@OptIn(ExperimentalSerializationApi::class)
@Serializable
@JsonClassDiscriminator("type_")
sealed interface NotificationData : DatatypeRoot
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,11 @@ package it.vercruysse.lemmyapi.datatypes
import it.vercruysse.lemmyapi.CommonParcelize
import it.vercruysse.lemmyapi.DatatypeRoot
import kotlinx.serialization.ExperimentalSerializationApi
import kotlinx.serialization.Serializable
import kotlinx.serialization.json.JsonClassDiscriminator

@CommonParcelize
@OptIn(ExperimentalSerializationApi::class)
@Serializable
@JsonClassDiscriminator("type_")
sealed interface PostCommentCombinedView : DatatypeRoot
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,11 @@ package it.vercruysse.lemmyapi.datatypes
import it.vercruysse.lemmyapi.CommonParcelize
import it.vercruysse.lemmyapi.DatatypeRoot
import kotlinx.serialization.ExperimentalSerializationApi
import kotlinx.serialization.Serializable
import kotlinx.serialization.json.JsonClassDiscriminator

@CommonParcelize
@OptIn(ExperimentalSerializationApi::class)
@Serializable
@JsonClassDiscriminator("type_")
sealed interface PostOrCommentOrPrivateMessage : DatatypeRoot
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,11 @@ package it.vercruysse.lemmyapi.datatypes
import it.vercruysse.lemmyapi.CommonParcelize
import it.vercruysse.lemmyapi.DatatypeRoot
import kotlinx.serialization.ExperimentalSerializationApi
import kotlinx.serialization.Serializable
import kotlinx.serialization.json.JsonClassDiscriminator

@CommonParcelize
@OptIn(ExperimentalSerializationApi::class)
@Serializable
@JsonClassDiscriminator("type_")
sealed interface ReportCombinedView : DatatypeRoot
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,11 @@ package it.vercruysse.lemmyapi.datatypes
import it.vercruysse.lemmyapi.CommonParcelize
import it.vercruysse.lemmyapi.DatatypeRoot
import kotlinx.serialization.ExperimentalSerializationApi
import kotlinx.serialization.Serializable
import kotlinx.serialization.json.JsonClassDiscriminator

@CommonParcelize
@OptIn(ExperimentalSerializationApi::class)
@Serializable
@JsonClassDiscriminator("type_")
sealed interface ResolveObjectView : DatatypeRoot
66 changes: 66 additions & 0 deletions app/src/jvmTest/kotlin/SealedInterfaceSerializationTest.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
import it.vercruysse.lemmyapi.IGNORE_UNKNOWN_KEYS_JSON
import kotlin.test.Test
import kotlin.test.assertEquals
import kotlin.test.assertIs
import kotlin.test.assertTrue
import it.vercruysse.lemmyapi.datatypes.PersonView as UniPersonView
import it.vercruysse.lemmyapi.datatypes.ResolveObjectView as UniResolveObjectView
import it.vercruysse.lemmyapi.datatypes.SearchResponse as UniSearchResponse
import it.vercruysse.lemmyapi.v1.x0.x0.datatypes.PersonView as V1PersonView
import it.vercruysse.lemmyapi.v1.x0.x0.datatypes.ResolveObjectView as V1ResolveObjectView
import it.vercruysse.lemmyapi.v1.x0.x0.datatypes.SearchResponse as V1SearchResponse

class SealedInterfaceSerializationTest {
private val searchResponseJson =
"""
{
"resolve": {
"type_": "person",
"person": {
"id": 1, "name": "name", "published_at": "2025-01-01T00:00:00Z", "ap_id": "https://example.com/u/name",
"local": true, "last_refreshed_at": "2025-01-01T00:00:00Z", "deleted": false, "bot_account": false,
"instance_id": 1, "post_count": 0, "comment_count": 0
},
"is_admin": false,
"banned": false
},
"comments": [], "posts": [], "communities": [], "persons": [], "multi_communities": []
}
""".trimIndent()

@Test
fun `generated v1 sealed interface is resolved by its class discriminator`() {
val response = IGNORE_UNKNOWN_KEYS_JSON.decodeFromString<V1SearchResponse>(searchResponseJson)

val resolved = assertIs<V1PersonView>(response.resolve)
assertEquals("name", resolved.person.name)
}

@Test
fun `generated v1 sealed interface is encoded with its class discriminator`() {
val response = IGNORE_UNKNOWN_KEYS_JSON.decodeFromString<V1SearchResponse>(searchResponseJson)

val encoded = IGNORE_UNKNOWN_KEYS_JSON.encodeToString<V1ResolveObjectView>(response.resolve!!)

assertTrue("\"type_\":\"person\"" in encoded, encoded)
}

@Test
fun `unified sealed interface is resolved by its class discriminator`() {
val response = IGNORE_UNKNOWN_KEYS_JSON.decodeFromString<UniSearchResponse>(searchResponseJson)

val resolved = assertIs<UniPersonView>(response.resolve)
assertEquals("name", resolved.person.name)
}

@Test
fun `unified sealed interface survives a round trip`() {
val response = IGNORE_UNKNOWN_KEYS_JSON.decodeFromString<UniSearchResponse>(searchResponseJson)

val roundTripped =
IGNORE_UNKNOWN_KEYS_JSON.decodeFromString<UniSearchResponse>(IGNORE_UNKNOWN_KEYS_JSON.encodeToString(response))

assertEquals(response, roundTripped)
assertIs<UniResolveObjectView>(roundTripped.resolve)
}
}
Loading