diff --git a/shared/common/build.gradle.kts b/shared/common/build.gradle.kts index 66a8aa6e..4ec1158d 100644 --- a/shared/common/build.gradle.kts +++ b/shared/common/build.gradle.kts @@ -61,6 +61,9 @@ dependencies { testImplementation("io.github.classgraph:classgraph:4.8.184") testImplementation("org.jetbrains.kotlinx:kotlinx-coroutines-test:1.10.2") testImplementation("io.mockk:mockk:1.13.9") + testImplementation("com.fasterxml.jackson.module:jackson-module-kotlin:2.17.0") + testImplementation("org.reflections:reflections:0.10.2") + } diff --git a/shared/common/src/main/kotlin/no/iktdev/mediaprocessing/shared/common/event_task_contract/TaskResultEvent.kt b/shared/common/src/main/kotlin/no/iktdev/mediaprocessing/shared/common/event_task_contract/TaskResultEvent.kt index bea868f3..f00cbf42 100644 --- a/shared/common/src/main/kotlin/no/iktdev/mediaprocessing/shared/common/event_task_contract/TaskResultEvent.kt +++ b/shared/common/src/main/kotlin/no/iktdev/mediaprocessing/shared/common/event_task_contract/TaskResultEvent.kt @@ -6,7 +6,7 @@ import no.iktdev.eventi.models.store.TaskStatus /** * Base class, should not be serialized into */ -abstract class TaskResultEvent( - open val status: TaskStatus, - open val error: String? = null +open class TaskResultEvent( + val status: TaskStatus, + val error: String? = null ) : Event() diff --git a/shared/common/src/main/kotlin/no/iktdev/mediaprocessing/shared/common/event_task_contract/events/ConvertTaskResultEvent.kt b/shared/common/src/main/kotlin/no/iktdev/mediaprocessing/shared/common/event_task_contract/events/ConvertTaskResultEvent.kt index 6a0ed47a..b6360b2b 100644 --- a/shared/common/src/main/kotlin/no/iktdev/mediaprocessing/shared/common/event_task_contract/events/ConvertTaskResultEvent.kt +++ b/shared/common/src/main/kotlin/no/iktdev/mediaprocessing/shared/common/event_task_contract/events/ConvertTaskResultEvent.kt @@ -3,10 +3,10 @@ package no.iktdev.mediaprocessing.shared.common.event_task_contract.events import no.iktdev.eventi.models.store.TaskStatus import no.iktdev.mediaprocessing.shared.common.event_task_contract.TaskResultEvent -data class ConvertTaskResultEvent( +class ConvertTaskResultEvent( val data: ConvertedData?, - override val status: TaskStatus, - override val error: String? = null, + status: TaskStatus, + error: String? = null, ): TaskResultEvent(status, error) { data class ConvertedData( val language: String, diff --git a/shared/common/src/main/kotlin/no/iktdev/mediaprocessing/shared/common/event_task_contract/events/CoordinatorReadStreamsResultEvent.kt b/shared/common/src/main/kotlin/no/iktdev/mediaprocessing/shared/common/event_task_contract/events/CoordinatorReadStreamsResultEvent.kt index 939406b4..07890eeb 100644 --- a/shared/common/src/main/kotlin/no/iktdev/mediaprocessing/shared/common/event_task_contract/events/CoordinatorReadStreamsResultEvent.kt +++ b/shared/common/src/main/kotlin/no/iktdev/mediaprocessing/shared/common/event_task_contract/events/CoordinatorReadStreamsResultEvent.kt @@ -4,8 +4,8 @@ import com.google.gson.JsonObject import no.iktdev.eventi.models.store.TaskStatus import no.iktdev.mediaprocessing.shared.common.event_task_contract.TaskResultEvent -data class CoordinatorReadStreamsResultEvent( +class CoordinatorReadStreamsResultEvent( val data: JsonObject? = null, - override val status: TaskStatus, - override val error: String? = null + status: TaskStatus, + error: String? = null ) : TaskResultEvent(status, error) diff --git a/shared/common/src/main/kotlin/no/iktdev/mediaprocessing/shared/common/event_task_contract/events/CoverDownloadResultEvent.kt b/shared/common/src/main/kotlin/no/iktdev/mediaprocessing/shared/common/event_task_contract/events/CoverDownloadResultEvent.kt index f0203ca4..d0dc9c39 100644 --- a/shared/common/src/main/kotlin/no/iktdev/mediaprocessing/shared/common/event_task_contract/events/CoverDownloadResultEvent.kt +++ b/shared/common/src/main/kotlin/no/iktdev/mediaprocessing/shared/common/event_task_contract/events/CoverDownloadResultEvent.kt @@ -3,10 +3,10 @@ package no.iktdev.mediaprocessing.shared.common.event_task_contract.events import no.iktdev.eventi.models.store.TaskStatus import no.iktdev.mediaprocessing.shared.common.event_task_contract.TaskResultEvent -data class CoverDownloadResultEvent( +class CoverDownloadResultEvent( val data: CoverDownloadedData? = null, - override val status: TaskStatus, - override val error: String? = null + status: TaskStatus, + error: String? = null ) : TaskResultEvent(status, error){ data class CoverDownloadedData( val source: String, diff --git a/shared/common/src/main/kotlin/no/iktdev/mediaprocessing/shared/common/event_task_contract/events/MetadataSearchResultEvent.kt b/shared/common/src/main/kotlin/no/iktdev/mediaprocessing/shared/common/event_task_contract/events/MetadataSearchResultEvent.kt index bdacf9e2..6d730383 100644 --- a/shared/common/src/main/kotlin/no/iktdev/mediaprocessing/shared/common/event_task_contract/events/MetadataSearchResultEvent.kt +++ b/shared/common/src/main/kotlin/no/iktdev/mediaprocessing/shared/common/event_task_contract/events/MetadataSearchResultEvent.kt @@ -6,11 +6,11 @@ import no.iktdev.mediaprocessing.shared.common.event_task_contract.TaskResultEve import no.iktdev.mediaprocessing.shared.common.model.MediaType import java.util.* -data class MetadataSearchResultEvent( +class MetadataSearchResultEvent( val results: List = emptyList(), val recommended: SearchResult? = null, - override val status: TaskStatus, - override val error: String? = null + status: TaskStatus, + error: String? = null ) : TaskResultEvent(status, error) { data class SearchResult( val simpleScore: Int, diff --git a/shared/common/src/main/kotlin/no/iktdev/mediaprocessing/shared/common/event_task_contract/events/MigrateContentToStoreTaskResultEvent.kt b/shared/common/src/main/kotlin/no/iktdev/mediaprocessing/shared/common/event_task_contract/events/MigrateContentToStoreTaskResultEvent.kt index b876bc53..e12d5f93 100644 --- a/shared/common/src/main/kotlin/no/iktdev/mediaprocessing/shared/common/event_task_contract/events/MigrateContentToStoreTaskResultEvent.kt +++ b/shared/common/src/main/kotlin/no/iktdev/mediaprocessing/shared/common/event_task_contract/events/MigrateContentToStoreTaskResultEvent.kt @@ -4,13 +4,13 @@ import no.iktdev.eventi.models.store.TaskStatus import no.iktdev.mediaprocessing.shared.common.event_task_contract.TaskResultEvent import no.iktdev.mediaprocessing.shared.common.model.MigrateStatus -data class MigrateContentToStoreTaskResultEvent( +class MigrateContentToStoreTaskResultEvent( val collection: String, val videoMigrate: FileMigration, val subtitleMigrate: List, val coverMigrate: List, - override val status: TaskStatus, - override val error: String? = null + status: TaskStatus, + error: String? = null ) : TaskResultEvent(status, error) { data class FileMigration( val storedUri: String?, diff --git a/shared/common/src/main/kotlin/no/iktdev/mediaprocessing/shared/common/event_task_contract/events/ProcesserEncodeResultEvent.kt b/shared/common/src/main/kotlin/no/iktdev/mediaprocessing/shared/common/event_task_contract/events/ProcesserEncodeResultEvent.kt index b3be6ab8..20f69c70 100644 --- a/shared/common/src/main/kotlin/no/iktdev/mediaprocessing/shared/common/event_task_contract/events/ProcesserEncodeResultEvent.kt +++ b/shared/common/src/main/kotlin/no/iktdev/mediaprocessing/shared/common/event_task_contract/events/ProcesserEncodeResultEvent.kt @@ -3,10 +3,10 @@ package no.iktdev.mediaprocessing.shared.common.event_task_contract.events import no.iktdev.eventi.models.store.TaskStatus import no.iktdev.mediaprocessing.shared.common.event_task_contract.TaskResultEvent -data class ProcesserEncodeResultEvent( +class ProcesserEncodeResultEvent( val data: EncodeResult? = null, - override val status: TaskStatus, - override val error: String? = null + status: TaskStatus, + error: String? = null ) : TaskResultEvent(status, error) { data class EncodeResult( val cachedOutputFile: String? = null diff --git a/shared/common/src/main/kotlin/no/iktdev/mediaprocessing/shared/common/event_task_contract/events/ProcesserExtractResultEvent.kt b/shared/common/src/main/kotlin/no/iktdev/mediaprocessing/shared/common/event_task_contract/events/ProcesserExtractResultEvent.kt index 60106ea1..406c018d 100644 --- a/shared/common/src/main/kotlin/no/iktdev/mediaprocessing/shared/common/event_task_contract/events/ProcesserExtractResultEvent.kt +++ b/shared/common/src/main/kotlin/no/iktdev/mediaprocessing/shared/common/event_task_contract/events/ProcesserExtractResultEvent.kt @@ -3,10 +3,10 @@ package no.iktdev.mediaprocessing.shared.common.event_task_contract.events import no.iktdev.eventi.models.store.TaskStatus import no.iktdev.mediaprocessing.shared.common.event_task_contract.TaskResultEvent -data class ProcesserExtractResultEvent( +class ProcesserExtractResultEvent( val data: ExtractResult? = null, - override val status: TaskStatus, - override val error: String? = null + status: TaskStatus, + error: String? = null ) : TaskResultEvent(status, error) { data class ExtractResult( val language: String, diff --git a/shared/common/src/main/kotlin/no/iktdev/mediaprocessing/shared/common/event_task_contract/events/StoreContentAndMetadataTaskResultEvent.kt b/shared/common/src/main/kotlin/no/iktdev/mediaprocessing/shared/common/event_task_contract/events/StoreContentAndMetadataTaskResultEvent.kt index 42cfe188..c06137fc 100644 --- a/shared/common/src/main/kotlin/no/iktdev/mediaprocessing/shared/common/event_task_contract/events/StoreContentAndMetadataTaskResultEvent.kt +++ b/shared/common/src/main/kotlin/no/iktdev/mediaprocessing/shared/common/event_task_contract/events/StoreContentAndMetadataTaskResultEvent.kt @@ -3,8 +3,8 @@ package no.iktdev.mediaprocessing.shared.common.event_task_contract.events import no.iktdev.eventi.models.store.TaskStatus import no.iktdev.mediaprocessing.shared.common.event_task_contract.TaskResultEvent -data class StoreContentAndMetadataTaskResultEvent( - override val status: TaskStatus, - override val error: String? = null +class StoreContentAndMetadataTaskResultEvent( + status: TaskStatus, + error: String? = null ) : TaskResultEvent(status, error){ } \ No newline at end of file diff --git a/shared/common/src/test/kotlin/no/iktdev/mediaprocessing/shared/common/event_task_contract/TaskResultEventSerializationTest.kt b/shared/common/src/test/kotlin/no/iktdev/mediaprocessing/shared/common/event_task_contract/TaskResultEventSerializationTest.kt new file mode 100644 index 00000000..d255af85 --- /dev/null +++ b/shared/common/src/test/kotlin/no/iktdev/mediaprocessing/shared/common/event_task_contract/TaskResultEventSerializationTest.kt @@ -0,0 +1,116 @@ +package no.iktdev.mediaprocessing.shared.common.event_task_contract + +import com.fasterxml.jackson.databind.ObjectMapper +import com.fasterxml.jackson.databind.SerializationFeature +import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule +import com.fasterxml.jackson.module.kotlin.KotlinFeature +import com.fasterxml.jackson.module.kotlin.KotlinModule +import com.google.gson.GsonBuilder +import com.google.gson.JsonDeserializer +import com.google.gson.JsonPrimitive +import com.google.gson.JsonSerializer +import no.iktdev.eventi.models.store.TaskStatus +import org.junit.jupiter.api.Test +import org.junit.jupiter.api.assertDoesNotThrow +import org.reflections.Reflections +import java.time.Instant +import java.util.* +import kotlin.reflect.KClass +import kotlin.reflect.full.primaryConstructor + +class TaskResultEventSerializationTest { + + val gson = GsonBuilder() + .registerTypeAdapter(Instant::class.java, JsonSerializer { src, _, _ -> + JsonPrimitive(src.toString()) + }) + .registerTypeAdapter(Instant::class.java, JsonDeserializer { json, _, _ -> + Instant.parse(json.asString) + }) + .create() + + val jackson = ObjectMapper() + .registerModule(JavaTimeModule()) + .registerModule( + KotlinModule.Builder() + .configure(KotlinFeature.NullToEmptyCollection, false) + .configure(KotlinFeature.NullToEmptyMap, false) + .configure(KotlinFeature.NullIsSameAsDefault, false) + .configure(KotlinFeature.StrictNullChecks, true) + .configure(KotlinFeature.UseJavaDurationConversion, true) + .build() + ) + .disable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS) + + + @Test + fun `all TaskResultEvent subclasses must serialize with Gson and Jackson`() { + val reflections = Reflections("no.iktdev.mediaprocessing") // rotpakken din + val subclasses = reflections.getSubTypesOf(TaskResultEvent::class.java) + + require(subclasses.isNotEmpty()) { + "Fant ingen subklasser av TaskResultEvent — er pakken riktig?" + } + + subclasses.forEach { clazz -> + assertDoesNotThrow("Serialization failed for ${clazz.simpleName}") { + val instance = createDummyInstance(clazz) + val jsonGson = gson.toJson(instance) + gson.fromJson(jsonGson, clazz) + + val jsonJackson = jackson.writeValueAsString(instance) + jackson.readValue(jsonJackson, clazz) + } + } + } + + /** + * Lager en dummy-instans av en TaskResultEvent-subklasse. + * Forutsetter at alle event-klasser har en primary constructor. + */ + private fun createDummyInstance(clazz: Class<*>): Any { + val kClass = clazz.kotlin + val ctor = kClass.primaryConstructor + ?: error("Klassen ${clazz.simpleName} mangler primary constructor") + + val args = ctor.parameters.associateWith { param -> + val kType = param.type + val classifier = kType.classifier + + // --- 1. Handle concrete known types first --- + if (classifier == String::class) return@associateWith "dummy" + if (classifier == Int::class) return@associateWith 1 + if (classifier == Long::class) return@associateWith 1L + if (classifier == Boolean::class) return@associateWith false + if (classifier == Double::class) return@associateWith 1.0 + if (classifier == UUID::class) return@associateWith UUID.randomUUID() + if (classifier == Instant::class) return@associateWith Instant.now() + if (classifier == TaskStatus::class) return@associateWith TaskStatus.Completed + if (classifier == Float::class) return@associateWith 1.0f + + + // --- 2. Generic enum support --- + if (classifier is KClass<*> && classifier.java.isEnum) { + return@associateWith classifier.java.enumConstants.first() + } + + // --- 3. Lists and maps --- + if (classifier == List::class) return@associateWith emptyList() + if (classifier == Map::class) return@associateWith emptyMap() + + // --- 4. Nested data classes --- + if (classifier is KClass<*> && classifier.isData) { + return@associateWith createDummyInstance(classifier.java) + } + + // --- 5. Fallback --- + null + } + + return ctor.callBy(args) + } + + + + +}