From 7da610ef7f0e4959304c949f41ccffbcdd174c74 Mon Sep 17 00:00:00 2001 From: Melad Raouf Date: Mon, 12 Jan 2026 14:11:06 +0000 Subject: [PATCH] [MS-1293] Refactor feature/clientapi to use Kotlin Serialization instead of Jackson --- feature/client-api/build.gradle.kts | 2 +- .../extractors/ActionRequestExtractor.kt | 17 ++--- ...tEnrolmentCreationEventForRecordUseCase.kt | 13 +--- .../extractors/ActionRequestExtractorTest.kt | 62 +++++++++++++++++++ ...olmentCreationEventForRecordUseCaseTest.kt | 3 +- .../simprints/core/tools/json/JsonHelper.kt | 8 ++- 6 files changed, 82 insertions(+), 23 deletions(-) create mode 100644 feature/client-api/src/test/java/com/simprints/feature/clientapi/mappers/request/extractors/ActionRequestExtractorTest.kt diff --git a/feature/client-api/build.gradle.kts b/feature/client-api/build.gradle.kts index 2437efe1f7..fcf37de4d6 100644 --- a/feature/client-api/build.gradle.kts +++ b/feature/client-api/build.gradle.kts @@ -1,6 +1,7 @@ plugins { id("simprints.feature") id("kotlin-parcelize") + id("simprints.library.kotlinSerialization") } android { namespace = "com.simprints.feature.clientapi" @@ -15,5 +16,4 @@ dependencies { implementation(project(":infra:logging-persistent")) implementation(libs.libsimprints) - implementation(libs.jackson.core) } diff --git a/feature/client-api/src/main/java/com/simprints/feature/clientapi/mappers/request/extractors/ActionRequestExtractor.kt b/feature/client-api/src/main/java/com/simprints/feature/clientapi/mappers/request/extractors/ActionRequestExtractor.kt index 3f20846bb1..b4bd908975 100644 --- a/feature/client-api/src/main/java/com/simprints/feature/clientapi/mappers/request/extractors/ActionRequestExtractor.kt +++ b/feature/client-api/src/main/java/com/simprints/feature/clientapi/mappers/request/extractors/ActionRequestExtractor.kt @@ -1,10 +1,12 @@ package com.simprints.feature.clientapi.mappers.request.extractors -import android.content.Intent import com.simprints.core.tools.json.JsonHelper import com.simprints.feature.clientapi.extensions.extractString import com.simprints.feature.clientapi.models.ClientApiConstants import com.simprints.libsimprints.Constants +import kotlinx.serialization.json.JsonElement +import kotlinx.serialization.json.intOrNull +import kotlinx.serialization.json.jsonPrimitive internal abstract class ActionRequestExtractor( private val extras: Map, @@ -30,14 +32,15 @@ internal abstract class ActionRequestExtractor( open fun getMetadata(): String = extras.extractString(Constants.SIMPRINTS_METADATA) - open fun getSubjectAge(): Int? = try { - val parsedMetadata = JsonHelper.fromJson>(getMetadata()) - parsedMetadata[Constants.SIMPRINTS_SUBJECT_AGE] as? Int - } catch (e: Exception) { + fun getSubjectAge(): Int? = try { + val parsedMetadata = + JsonHelper.json.decodeFromString>(getMetadata()) + parsedMetadata[Constants.SIMPRINTS_SUBJECT_AGE] + ?.jsonPrimitive + ?.intOrNull + } catch (_: Exception) { null } - protected open fun Intent.extractString(key: String): String = this.getStringExtra(key) ?: "" - open fun getUnknownExtras(): Map = extras.filter { it.key.isNotBlank() && !expectedKeys.contains(it.key) } } diff --git a/feature/client-api/src/main/java/com/simprints/feature/clientapi/usecases/GetEnrolmentCreationEventForRecordUseCase.kt b/feature/client-api/src/main/java/com/simprints/feature/clientapi/usecases/GetEnrolmentCreationEventForRecordUseCase.kt index 58b755ba91..d3b9a1648f 100644 --- a/feature/client-api/src/main/java/com/simprints/feature/clientapi/usecases/GetEnrolmentCreationEventForRecordUseCase.kt +++ b/feature/client-api/src/main/java/com/simprints/feature/clientapi/usecases/GetEnrolmentCreationEventForRecordUseCase.kt @@ -1,9 +1,5 @@ package com.simprints.feature.clientapi.usecases -import com.fasterxml.jackson.databind.module.SimpleModule -import com.simprints.core.domain.tokenization.TokenizableString -import com.simprints.core.domain.tokenization.serialization.TokenizationClassNameDeserializer -import com.simprints.core.domain.tokenization.serialization.TokenizationClassNameSerializer import com.simprints.core.tools.json.JsonHelper import com.simprints.core.tools.utils.EncodingUtils import com.simprints.infra.config.store.ConfigRepository @@ -46,7 +42,7 @@ internal class GetEnrolmentCreationEventForRecordUseCase @Inject constructor( return null } - return jsonHelper.toJson(CoSyncEnrolmentRecordEvents(listOf(recordCreationEvent)), coSyncSerializationModule) + return jsonHelper.json.encodeToString(CoSyncEnrolmentRecordEvents(listOf(recordCreationEvent))) } private fun EnrolmentRecord.fromSubjectToEnrolmentCreationEvent() = EnrolmentRecordCreationEvent( @@ -57,11 +53,4 @@ internal class GetEnrolmentCreationEventForRecordUseCase @Inject constructor( biometricReferences = EnrolmentRecordCreationEvent.buildBiometricReferences(references, encoder), externalCredentials = externalCredentials, ) - - companion object { - val coSyncSerializationModule = SimpleModule().apply { - addSerializer(TokenizableString::class.java, TokenizationClassNameSerializer()) - addDeserializer(TokenizableString::class.java, TokenizationClassNameDeserializer()) - } - } } diff --git a/feature/client-api/src/test/java/com/simprints/feature/clientapi/mappers/request/extractors/ActionRequestExtractorTest.kt b/feature/client-api/src/test/java/com/simprints/feature/clientapi/mappers/request/extractors/ActionRequestExtractorTest.kt new file mode 100644 index 0000000000..4b00f3f1b4 --- /dev/null +++ b/feature/client-api/src/test/java/com/simprints/feature/clientapi/mappers/request/extractors/ActionRequestExtractorTest.kt @@ -0,0 +1,62 @@ +package com.simprints.feature.clientapi.mappers.request.extractors + +import com.google.common.truth.Truth.assertThat +import com.simprints.libsimprints.Constants.SIMPRINTS_METADATA +import com.simprints.libsimprints.Constants.SIMPRINTS_SUBJECT_AGE +import kotlin.test.Test + +internal class ActionRequestExtractorTest { + // Concrete subclass for testing + class TestActionRequestExtractor( + extras: Map, + override val expectedKeys: List = emptyList(), + ) : ActionRequestExtractor(extras) + + @Test + fun `returns age when valid integer present`() { + val extractor = TestActionRequestExtractor( + mapOf(SIMPRINTS_METADATA to """{"$SIMPRINTS_SUBJECT_AGE": 30}"""), + ) + assertThat(extractor.getSubjectAge()).isEqualTo(30) + } + + @Test + fun `returns null when age key is missing`() { + val extractor = TestActionRequestExtractor( + mapOf(SIMPRINTS_METADATA to """{"name": "Alice"}"""), + ) + assertThat(extractor.getSubjectAge()).isNull() + } + + @Test + fun `returns null when age key is not an integer`() { + val extractor = TestActionRequestExtractor( + mapOf(SIMPRINTS_METADATA to """{"$SIMPRINTS_SUBJECT_AGE": "twenty"}"""), + ) + assertThat(extractor.getSubjectAge()).isNull() + } + + @Test + fun `returns null for invalid JSON`() { + val extractor = TestActionRequestExtractor( + mapOf(SIMPRINTS_METADATA to """{invalid json"""), + ) + assertThat(extractor.getSubjectAge()).isNull() + } + + @Test + fun `returns null when age is null in JSON`() { + val extractor = TestActionRequestExtractor( + mapOf(SIMPRINTS_METADATA to """{"$SIMPRINTS_SUBJECT_AGE": null}"""), + ) + assertThat(extractor.getSubjectAge()).isNull() + } + + @Test + fun `returns null when metadata key is missing`() { + val extractor = TestActionRequestExtractor( + emptyMap(), + ) + assertThat(extractor.getSubjectAge()).isNull() + } +} diff --git a/feature/client-api/src/test/java/com/simprints/feature/clientapi/usecases/GetEnrolmentCreationEventForRecordUseCaseTest.kt b/feature/client-api/src/test/java/com/simprints/feature/clientapi/usecases/GetEnrolmentCreationEventForRecordUseCaseTest.kt index 0d27a29664..c9d10e7322 100644 --- a/feature/client-api/src/test/java/com/simprints/feature/clientapi/usecases/GetEnrolmentCreationEventForRecordUseCaseTest.kt +++ b/feature/client-api/src/test/java/com/simprints/feature/clientapi/usecases/GetEnrolmentCreationEventForRecordUseCaseTest.kt @@ -6,6 +6,7 @@ import com.simprints.core.tools.utils.EncodingUtils import com.simprints.infra.config.store.ConfigRepository import com.simprints.infra.config.store.models.UpSynchronizationConfiguration import com.simprints.infra.enrolment.records.repository.EnrolmentRecordRepository +import com.simprints.infra.events.event.cosync.CoSyncEnrolmentRecordEvents import com.simprints.testtools.common.coroutines.TestCoroutineRule import io.mockk.* import io.mockk.impl.annotations.MockK @@ -94,7 +95,7 @@ class GetEnrolmentCreationEventForRecordUseCaseTest { val result = useCase("projectId", "subjectId") coVerify { enrolmentRecordRepository.load(any()) } - coVerify { jsonHelper.toJson(any(), any()) } + coVerify { jsonHelper.json.encodeToString(any()) } assertThat(result).isNotNull() } } diff --git a/infra/core/src/main/java/com/simprints/core/tools/json/JsonHelper.kt b/infra/core/src/main/java/com/simprints/core/tools/json/JsonHelper.kt index b779496d3d..7c5269a56f 100644 --- a/infra/core/src/main/java/com/simprints/core/tools/json/JsonHelper.kt +++ b/infra/core/src/main/java/com/simprints/core/tools/json/JsonHelper.kt @@ -7,6 +7,7 @@ import com.fasterxml.jackson.databind.JavaType import com.fasterxml.jackson.databind.Module import com.fasterxml.jackson.databind.ObjectMapper import com.fasterxml.jackson.module.kotlin.registerKotlinModule +import kotlinx.serialization.SerializationException import kotlinx.serialization.json.Json object JsonHelper { @@ -59,8 +60,11 @@ object JsonHelper { inline fun fromJson(json: String): T = jackson.readValue(json, T::class.java) - fun validateJsonOrThrow(json: String) { - jackson.readTree(json) + /** + * @throws SerializationException if the JSON is not valid + */ + fun validateJsonOrThrow(jsonString: String) { + json.parseToJsonElement(jsonString) } // Todo will be replacing the above fromJson and toJson once completely removing jackson before the 2026.1.0 release