diff --git a/.github/workflows/lint_unitTests_build.yml b/.github/workflows/lint_unitTests_build.yml index 73064f50..a1a3cc00 100644 --- a/.github/workflows/lint_unitTests_build.yml +++ b/.github/workflows/lint_unitTests_build.yml @@ -50,6 +50,9 @@ jobs: - name: lint check run: ./gradlew --no-daemon lintDebug + - name: detekt check + run: ./gradlew --no-daemon detektDebug + unit: runs-on: ubuntu-latest steps: diff --git a/detekt-rules/build.gradle b/detekt-rules/build.gradle new file mode 100644 index 00000000..9bd9ef6b --- /dev/null +++ b/detekt-rules/build.gradle @@ -0,0 +1,27 @@ +apply plugin: 'java-library' +apply plugin: 'kotlin' + +java { + sourceCompatibility = JavaVersion.VERSION_11 + targetCompatibility = JavaVersion.VERSION_11 +} + +compileKotlin { + kotlinOptions { + jvmTarget = '11' + } +} + +compileTestKotlin { + kotlinOptions { + jvmTarget = '11' + } +} + +dependencies { + implementation libs.detekt.api + implementation libs.kotlin.stdlib + + testImplementation libs.detekt.test + testImplementation libs.junit +} diff --git a/detekt-rules/src/main/kotlin/cloud/mindbox/detekt/GsonSerializedNameRule.kt b/detekt-rules/src/main/kotlin/cloud/mindbox/detekt/GsonSerializedNameRule.kt new file mode 100644 index 00000000..3d5cc9ac --- /dev/null +++ b/detekt-rules/src/main/kotlin/cloud/mindbox/detekt/GsonSerializedNameRule.kt @@ -0,0 +1,173 @@ +package cloud.mindbox.detekt + +import io.gitlab.arturbosch.detekt.api.CodeSmell +import io.gitlab.arturbosch.detekt.api.Config +import io.gitlab.arturbosch.detekt.api.Debt +import io.gitlab.arturbosch.detekt.api.Entity +import io.gitlab.arturbosch.detekt.api.Issue +import io.gitlab.arturbosch.detekt.api.Rule +import io.gitlab.arturbosch.detekt.api.Severity +import org.jetbrains.kotlin.descriptors.ClassDescriptor +import org.jetbrains.kotlin.psi.KtCallExpression +import org.jetbrains.kotlin.psi.KtClass +import org.jetbrains.kotlin.psi.KtClassLiteralExpression +import org.jetbrains.kotlin.psi.KtDotQualifiedExpression +import org.jetbrains.kotlin.psi.KtFile +import org.jetbrains.kotlin.psi.KtNamedFunction +import org.jetbrains.kotlin.psi.KtObjectDeclaration +import org.jetbrains.kotlin.psi.KtParameter +import org.jetbrains.kotlin.psi.psiUtil.collectDescendantsOfType +import org.jetbrains.kotlin.resolve.BindingContext +import org.jetbrains.kotlin.resolve.DescriptorToSourceUtils +import org.jetbrains.kotlin.resolve.calls.util.getResolvedCall +import org.jetbrains.kotlin.resolve.calls.util.getType +import org.jetbrains.kotlin.types.KotlinType + +class GsonSerializedNameRule(config: Config) : Rule(config) { + + override val issue: Issue = Issue( + id = "GsonMissingSerializedName", + severity = Severity.Defect, + description = "Gson-serialized Kotlin data class constructor properties must declare @SerializedName.", + debt = Debt.FIVE_MINS + ) + + private val reportedParameterKeys: MutableSet = mutableSetOf() + private val checkedClasses: MutableSet = mutableSetOf() + + override fun preVisit(root: KtFile) { + reportedParameterKeys.clear() + } + + override fun visitClass(klass: KtClass) { + super.visitClass(klass) + if (!klass.isData()) return + if (!klass.hasSerializedNameContract()) return + reportMissingSerializedNameParameters(klass) + } + + override fun visitCallExpression(expression: KtCallExpression) { + super.visitCallExpression(expression) + val calleeName = expression.calleeExpression?.text ?: return + if (calleeName in DIRECT_GSON_FUNCTION_NAMES) { + checkFirstArgumentType(expression) + checkTypeArguments(expression) + checkClassLiteralArguments(expression) + } else { + checkIfIndirectGsonCall(expression) + } + } + + override fun visitObjectDeclaration(declaration: KtObjectDeclaration) { + super.visitObjectDeclaration(declaration) + declaration.superTypeListEntries + .filter { entry -> entry.typeReference?.text?.contains(TYPE_TOKEN) == true } + .forEach { entry -> + val type = bindingContext[BindingContext.TYPE, entry.typeReference] ?: return@forEach + type.arguments + .filterNot { projection -> projection.isStarProjection } + .forEach { projection -> checkKotlinType(projection.type) } + } + } + + private fun checkIfIndirectGsonCall(expression: KtCallExpression) { + val resolvedCall = expression.getResolvedCall(bindingContext) ?: return + val sourceFunction = DescriptorToSourceUtils + .descriptorToDeclaration(resolvedCall.resultingDescriptor) as? KtNamedFunction ?: return + if (sourceFunction.typeParameters.isEmpty()) return + if (!sourceFunction.containsDirectGsonCall()) return + checkFirstArgumentType(expression) + checkTypeArguments(expression) + checkClassLiteralArguments(expression) + } + + private fun KtNamedFunction.containsDirectGsonCall(): Boolean = + collectDescendantsOfType() + .any { call -> call.calleeExpression?.text in DIRECT_GSON_FUNCTION_NAMES } + + private fun checkFirstArgumentType(expression: KtCallExpression) { + val argument = expression.valueArguments.firstOrNull()?.getArgumentExpression() ?: return + checkKotlinType(argument.getType(bindingContext) ?: return) + } + + private fun checkTypeArguments(expression: KtCallExpression) { + expression.typeArguments + .mapNotNull { typeProjection -> typeProjection.typeReference } + .mapNotNull { typeRef -> bindingContext[BindingContext.TYPE, typeRef] } + .forEach { type -> checkKotlinType(type) } + } + + private fun checkClassLiteralArguments(expression: KtCallExpression) { + expression.valueArguments + .mapNotNull { argument -> argument.getArgumentExpression() } + .forEach { argument -> + val classLiteral = when (argument) { + is KtDotQualifiedExpression -> argument.receiverExpression as? KtClassLiteralExpression + is KtClassLiteralExpression -> argument + else -> null + } ?: return@forEach + val type = classLiteral.getType(bindingContext) ?: return@forEach + type.arguments.firstOrNull() + ?.takeIf { projection -> !projection.isStarProjection } + ?.type + ?.let(::checkKotlinType) + } + } + + private fun checkKotlinType(type: KotlinType) { + val descriptor = type.constructor.declarationDescriptor as? ClassDescriptor ?: return + val sourceClass = DescriptorToSourceUtils.descriptorToDeclaration(descriptor) as? KtClass + sourceClass?.let(::reportMissingSerializedNameParameters) + type.arguments + .filterNot { projection -> projection.isStarProjection } + .forEach { projection -> checkKotlinType(projection.type) } + } + + private fun reportMissingSerializedNameParameters(klass: KtClass) { + if (!klass.isData()) return + val qualifiedName = klass.fqName?.asString() ?: return + if (!checkedClasses.add(qualifiedName)) return + klass.primaryConstructorParameters + .filter { parameter -> parameter.valOrVarKeyword != null } + .forEach { parameter -> + if (!parameter.hasSerializedNameAnnotation()) reportParameter(parameter, klass) + val typeRef = parameter.typeReference ?: return@forEach + val type = bindingContext[BindingContext.TYPE, typeRef] ?: return@forEach + checkKotlinType(type) + } + } + + private fun reportParameter(parameter: KtParameter, klass: KtClass) { + val key = "${parameter.containingFile.name}:${parameter.textOffset}" + if (!reportedParameterKeys.add(key)) return + report( + CodeSmell( + issue = issue, + entity = Entity.from(parameter), + message = "${klass.name}.${parameter.name} must declare @SerializedName." + ) + ) + } + + private fun KtClass.hasSerializedNameContract(): Boolean { + return primaryConstructorParameters.any { parameter -> parameter.hasSerializedNameAnnotation() } + } + + private fun KtParameter.hasSerializedNameAnnotation(): Boolean { + return annotationEntries.any { annotationEntry -> + annotationEntry.shortName?.asString() == SERIALIZED_NAME + } + } + + private companion object { + private const val SERIALIZED_NAME = "SerializedName" + private const val TYPE_TOKEN = "TypeToken" + + private val DIRECT_GSON_FUNCTION_NAMES: Set = setOf( + "fromJson", + "fromJsonTree", + "toJson", + "toJsonTree", + ) + } +} diff --git a/detekt-rules/src/main/kotlin/cloud/mindbox/detekt/MindboxRuleSetProvider.kt b/detekt-rules/src/main/kotlin/cloud/mindbox/detekt/MindboxRuleSetProvider.kt new file mode 100644 index 00000000..1978f0bd --- /dev/null +++ b/detekt-rules/src/main/kotlin/cloud/mindbox/detekt/MindboxRuleSetProvider.kt @@ -0,0 +1,19 @@ +package cloud.mindbox.detekt + +import io.gitlab.arturbosch.detekt.api.Config +import io.gitlab.arturbosch.detekt.api.RuleSet +import io.gitlab.arturbosch.detekt.api.RuleSetProvider + +class MindboxRuleSetProvider : RuleSetProvider { + + override val ruleSetId: String = "mindbox" + + override fun instance(config: Config): RuleSet { + return RuleSet( + id = ruleSetId, + rules = listOf( + GsonSerializedNameRule(config = config), + ) + ) + } +} diff --git a/detekt-rules/src/main/resources/META-INF/services/io.gitlab.arturbosch.detekt.api.RuleSetProvider b/detekt-rules/src/main/resources/META-INF/services/io.gitlab.arturbosch.detekt.api.RuleSetProvider new file mode 100644 index 00000000..9f5c9241 --- /dev/null +++ b/detekt-rules/src/main/resources/META-INF/services/io.gitlab.arturbosch.detekt.api.RuleSetProvider @@ -0,0 +1 @@ +cloud.mindbox.detekt.MindboxRuleSetProvider diff --git a/detekt-rules/src/test/kotlin/cloud/mindbox/detekt/GsonSerializedNameRuleTest.kt b/detekt-rules/src/test/kotlin/cloud/mindbox/detekt/GsonSerializedNameRuleTest.kt new file mode 100644 index 00000000..395fbdc3 --- /dev/null +++ b/detekt-rules/src/test/kotlin/cloud/mindbox/detekt/GsonSerializedNameRuleTest.kt @@ -0,0 +1,276 @@ +package cloud.mindbox.detekt + +import io.github.detekt.test.utils.createEnvironment +import io.gitlab.arturbosch.detekt.api.Finding +import io.gitlab.arturbosch.detekt.test.TestConfig +import io.gitlab.arturbosch.detekt.test.compileAndLintWithContext +import org.junit.AfterClass +import org.junit.Assert.assertEquals +import org.junit.Assert.assertTrue +import org.junit.Test + +class GsonSerializedNameRuleTest { + + private val stubs = """ + annotation class SerializedName(val value: String) + abstract class TypeToken + fun fromJson(json: String, clazz: Class<*>): Any = error("stub") + fun fromJson(json: String, clazz: Class): T = error("stub") + fun toJson(obj: Any?): String = error("stub") + fun fromJsonTyped(json: String, clazz: Class): T = fromJson(json, clazz) + fun toJsonTyped(obj: T): String = toJson(obj) + fun operationBodyJson(obj: T): String = toJson(obj) + fun convertJsonToBody(json: String, clazz: Class): T = fromJson(json, clazz) + """.trimIndent() + + private fun compileAndLint(code: String): List = + GsonSerializedNameRule(TestConfig()).compileAndLintWithContext(env, code) + + @Test + fun `reports class passed via class literal to fromJson`() { + val findings = compileAndLint( + """ + $stubs + data class MyDto(val name: String) + fun test(json: String): Any = fromJson(json, MyDto::class.java) + """.trimIndent() + ) + assertEquals(1, findings.size) + assertTrue(findings.first().message.contains("MyDto.name")) + } + + @Test + fun `reports class passed as explicit type argument to fromJson`() { + val findings = compileAndLint( + """ + $stubs + data class MyDto(val name: String) + fun test(json: String): MyDto = fromJson(json, MyDto::class.java) + """.trimIndent() + ) + assertEquals(1, findings.size) + assertTrue(findings.first().message.contains("MyDto.name")) + } + + @Test + fun `reports class passed as first argument to toJson`() { + val findings = compileAndLint( + """ + $stubs + data class MyDto(val name: String) + fun test() = toJson(MyDto("x")) + """.trimIndent() + ) + assertEquals(1, findings.size) + assertTrue(findings.first().message.contains("MyDto.name")) + } + + @Test + fun `reports class when toJson called with this inside the class`() { + val findings = compileAndLint( + """ + $stubs + data class MyDto(val name: String) { + fun serialize() = toJson(this) + } + """.trimIndent() + ) + assertEquals(1, findings.size) + assertTrue(findings.first().message.contains("MyDto.name")) + } + + @Test + fun `reports class passed via operationBodyJson`() { + val findings = compileAndLint( + """ + $stubs + data class RequestBody(val action: String) + fun test() = operationBodyJson(RequestBody("click")) + """.trimIndent() + ) + assertEquals(1, findings.size) + assertTrue(findings.first().message.contains("RequestBody.action")) + } + + @Test + fun `reports class passed via fromJsonTyped class literal`() { + val findings = compileAndLint( + """ + $stubs + data class ResponseBody(val status: String) + fun test(json: String): ResponseBody = fromJsonTyped(json, ResponseBody::class.java) + """.trimIndent() + ) + assertEquals(1, findings.size) + assertTrue(findings.first().message.contains("ResponseBody.status")) + } + + @Test + fun `reports class passed via convertJsonToBody`() { + val findings = compileAndLint( + """ + $stubs + data class EventBody(val type: String) + fun test(json: String): Any = convertJsonToBody(json, EventBody::class.java) + """.trimIndent() + ) + assertEquals(1, findings.size) + assertTrue(findings.first().message.contains("EventBody.type")) + } + + @Test + fun `reports class via custom auto-detected generic wrapper`() { + val findings = compileAndLint( + """ + $stubs + data class Payload(val id: String) + fun myDeserializer(json: String, clazz: Class): T = fromJson(json, clazz) + fun test(json: String) = myDeserializer(json, Payload::class.java) + """.trimIndent() + ) + assertEquals(1, findings.size) + assertTrue(findings.first().message.contains("Payload.id")) + } + + @Test + fun `does not report class passed to a function that does not wrap Gson`() { + val findings = compileAndLint( + """ + $stubs + data class Config(val key: String) + fun jacksonDeserialize(json: String, clazz: Class): T = error("stub") + fun test(json: String) = jacksonDeserialize(json, Config::class.java) + """.trimIndent() + ) + assertTrue(findings.isEmpty()) + } + + @Test + fun `reports class used as direct TypeToken generic argument`() { + val findings = compileAndLint( + """ + $stubs + data class MyDto(val name: String) + val token = object : TypeToken() {} + """.trimIndent() + ) + assertEquals(1, findings.size) + assertTrue(findings.first().message.contains("MyDto.name")) + } + + @Test + fun `reports class nested inside List in TypeToken`() { + val findings = compileAndLint( + """ + $stubs + data class MyDto(val name: String) + val token = object : TypeToken>() {} + """.trimIndent() + ) + assertEquals(1, findings.size) + assertTrue(findings.first().message.contains("MyDto.name")) + } + + @Test + fun `reports transitive field type missing annotation`() { + val findings = compileAndLint( + """ + $stubs + data class Inner(val value: Int) + data class Outer(val inner: Inner) + fun test() = toJson(Outer(Inner(1))) + """.trimIndent() + ) + assertEquals(2, findings.size) + assertTrue(findings.any { it.message.contains("Outer.inner") }) + assertTrue(findings.any { it.message.contains("Inner.value") }) + } + + @Test + fun `reports class nested inside generic List field transitively`() { + val findings = compileAndLint( + """ + $stubs + data class Item(val id: Int) + data class Page(val items: List) + fun test() = toJson(Page(emptyList())) + """.trimIndent() + ) + assertTrue(findings.any { it.message.contains("Page.items") }) + assertTrue(findings.any { it.message.contains("Item.id") }) + } + + @Test + fun `reports missing annotation when class has partial SerializedName contract`() { + val findings = compileAndLint( + """ + $stubs + data class MyDto( + @SerializedName("name") val name: String, + val age: Int + ) + """.trimIndent() + ) + assertEquals(1, findings.size) + assertTrue(findings.first().message.contains("MyDto.age")) + } + + @Test + fun `does not report data class not involved in any Gson call`() { + val findings = compileAndLint( + """ + $stubs + data class Config(val key: String, val value: Int) + """.trimIndent() + ) + assertTrue(findings.isEmpty()) + } + + @Test + fun `does not report when all fields have SerializedName`() { + val findings = compileAndLint( + """ + $stubs + data class MyDto( + @SerializedName("name") val name: String, + @SerializedName("age") val age: Int + ) + fun test() = toJson(MyDto("x", 1)) + """.trimIndent() + ) + assertTrue(findings.isEmpty()) + } + + @Test + fun `does not report non-data class passed to toJson`() { + val findings = compileAndLint( + """ + $stubs + class NotADataClass(val name: String) + fun test() = toJson(NotADataClass("x")) + """.trimIndent() + ) + assertTrue(findings.isEmpty()) + } + + @Test + fun `does not report stdlib types like String or List`() { + val findings = compileAndLint( + """ + $stubs + fun test() = toJson(listOf("a", "b")) + """.trimIndent() + ) + assertTrue(findings.isEmpty()) + } + + companion object { + private val environmentWrapper = createEnvironment() + private val env get() = environmentWrapper.env + + @AfterClass @JvmStatic + fun tearDown() { + environmentWrapper.dispose() + } + } +} diff --git a/detekt.yml b/detekt.yml new file mode 100644 index 00000000..81724dec --- /dev/null +++ b/detekt.yml @@ -0,0 +1,35 @@ +config: + validation: true + excludes: "mindbox.*,mindbox>.*>.*" + +comments: + active: false + +complexity: + active: false + +coroutines: + active: false + +empty-blocks: + active: false + +exceptions: + active: false + +naming: + active: false + +performance: + active: false + +potential-bugs: + active: false + +style: + active: false + +mindbox: + active: true + GsonMissingSerializedName: + active: true diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 604bf1db..256d8e47 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -41,6 +41,7 @@ ktlint-plugin = "12.1.1" ksp = "1.9.22-1.0.17" maven_publish = "0.32.0" kover = "0.8.3" +detekt = "1.23.6" pushclient = "7.2.0" @@ -54,6 +55,7 @@ buildscript-plugins = [ "ksp_gradle_plugin", "maven_publish_plugin", "kover_gradle_plugin", + "detekt_gradle_plugin", ] test = [ @@ -121,4 +123,7 @@ agcp = { module = "com.huawei.agconnect:agcp", version.ref = "agcp" } ktlint_gradle_plugin = { module = "org.jlleitschuh.gradle:ktlint-gradle", version.ref = "ktlint-plugin" } ksp_gradle_plugin = { module = "com.google.devtools.ksp:symbol-processing-gradle-plugin", version.ref = "ksp" } maven_publish_plugin = { module = "com.vanniktech:gradle-maven-publish-plugin", version.ref = "maven_publish" } -kover_gradle_plugin = { module = "org.jetbrains.kotlinx:kover-gradle-plugin", version.ref = "kover" } \ No newline at end of file +kover_gradle_plugin = { module = "org.jetbrains.kotlinx:kover-gradle-plugin", version.ref = "kover" } +detekt_gradle_plugin = { module = "io.gitlab.arturbosch.detekt:detekt-gradle-plugin", version.ref = "detekt" } +detekt_api = { module = "io.gitlab.arturbosch.detekt:detekt-api", version.ref = "detekt" } +detekt_test = { module = "io.gitlab.arturbosch.detekt:detekt-test", version.ref = "detekt" } diff --git a/modulesCommon.gradle b/modulesCommon.gradle index e1b436a4..54607db3 100644 --- a/modulesCommon.gradle +++ b/modulesCommon.gradle @@ -4,6 +4,7 @@ apply plugin: 'signing' apply plugin: 'org.jlleitschuh.gradle.ktlint' apply plugin: 'com.vanniktech.maven.publish' apply plugin: 'org.jetbrains.kotlinx.kover' +apply plugin: 'io.gitlab.arturbosch.detekt' group = 'com.github.mindbox-cloud' @@ -48,6 +49,16 @@ android { } } +detekt { + buildUponDefaultConfig = true + config.setFrom(files("${rootDir}/detekt.yml")) + source.setFrom(files("src/main/java", "src/main/kotlin")) +} + +dependencies { + detektPlugins project(path: ':detekt-rules', configuration: 'runtimeElements') +} + mavenPublishing { publishToMavenCentral("CENTRAL_PORTAL") diff --git a/sdk/src/main/java/cloud/mindbox/mobile_sdk/inapp/domain/models/GeoTargeting.kt b/sdk/src/main/java/cloud/mindbox/mobile_sdk/inapp/domain/models/GeoTargeting.kt index 1bb81a18..f58fca72 100644 --- a/sdk/src/main/java/cloud/mindbox/mobile_sdk/inapp/domain/models/GeoTargeting.kt +++ b/sdk/src/main/java/cloud/mindbox/mobile_sdk/inapp/domain/models/GeoTargeting.kt @@ -1,7 +1,9 @@ package cloud.mindbox.mobile_sdk.inapp.domain.models +import com.google.gson.annotations.SerializedName + internal data class GeoTargeting( - val cityId: String, - val regionId: String, - val countryId: String, + @SerializedName("cityId") val cityId: String, + @SerializedName("regionId") val regionId: String, + @SerializedName("countryId") val countryId: String, ) diff --git a/sdk/src/main/java/cloud/mindbox/mobile_sdk/inapp/domain/models/InAppFailuresWrapper.kt b/sdk/src/main/java/cloud/mindbox/mobile_sdk/inapp/domain/models/InAppFailuresWrapper.kt index 2596787a..c1426329 100644 --- a/sdk/src/main/java/cloud/mindbox/mobile_sdk/inapp/domain/models/InAppFailuresWrapper.kt +++ b/sdk/src/main/java/cloud/mindbox/mobile_sdk/inapp/domain/models/InAppFailuresWrapper.kt @@ -1,7 +1,8 @@ package cloud.mindbox.mobile_sdk.inapp.domain.models import cloud.mindbox.mobile_sdk.models.operation.request.InAppShowFailure +import com.google.gson.annotations.SerializedName internal data class InAppFailuresWrapper( - val failures: List + @SerializedName("failures") val failures: List ) diff --git a/sdk/src/main/java/cloud/mindbox/mobile_sdk/models/operation/CustomFields.kt b/sdk/src/main/java/cloud/mindbox/mobile_sdk/models/operation/CustomFields.kt index 8439d675..9cdafecb 100644 --- a/sdk/src/main/java/cloud/mindbox/mobile_sdk/models/operation/CustomFields.kt +++ b/sdk/src/main/java/cloud/mindbox/mobile_sdk/models/operation/CustomFields.kt @@ -13,6 +13,10 @@ public class CustomFields(public val fields: Map? = null) { * Convert [CustomFields] value to [T] typed object. * * @param classOfT Class type for result [CustomFields] object. + * + * **Important:** [T] is a caller-supplied type deserialized via Gson. All constructor + * properties of [T] must declare [@SerializedName][com.google.gson.annotations.SerializedName] + * to ensure correct serialization after code shrinking (ProGuard/R8). */ public fun convertTo(classOfT: Class): T? = LoggingExceptionHandler.runCatching(defaultValue = null) { val gson = Gson() diff --git a/sdk/src/main/java/cloud/mindbox/mobile_sdk/models/operation/response/InAppConfigResponse.kt b/sdk/src/main/java/cloud/mindbox/mobile_sdk/models/operation/response/InAppConfigResponse.kt index de99b37d..0a1ad6f6 100644 --- a/sdk/src/main/java/cloud/mindbox/mobile_sdk/models/operation/response/InAppConfigResponse.kt +++ b/sdk/src/main/java/cloud/mindbox/mobile_sdk/models/operation/response/InAppConfigResponse.kt @@ -75,7 +75,7 @@ internal data class SettingsDtoBlank( @JsonAdapter(FeatureTogglesDtoBlankDeserializer::class) internal data class FeatureTogglesDtoBlank( - val toggles: Map + @SerializedName("toggles") val toggles: Map ) } @@ -110,14 +110,14 @@ internal data class TtlDto( ) internal data class SlidingExpirationDto( - val config: Milliseconds?, - val pushTokenKeepalive: Milliseconds?, + @SerializedName("config") val config: Milliseconds?, + @SerializedName("pushTokenKeepalive") val pushTokenKeepalive: Milliseconds?, ) internal data class InappSettingsDto( - val maxInappsPerSession: Int?, - val maxInappsPerDay: Int?, - val minIntervalBetweenShows: Milliseconds?, + @SerializedName("maxInappsPerSession") val maxInappsPerSession: Int?, + @SerializedName("maxInappsPerDay") val maxInappsPerDay: Int?, + @SerializedName("minIntervalBetweenShows") val minIntervalBetweenShows: Milliseconds?, ) internal data class LogRequestDto( diff --git a/sdk/src/main/java/cloud/mindbox/mobile_sdk/pushes/MindboxRemoteMessage.kt b/sdk/src/main/java/cloud/mindbox/mobile_sdk/pushes/MindboxRemoteMessage.kt index b85d0d47..9083fad8 100644 --- a/sdk/src/main/java/cloud/mindbox/mobile_sdk/pushes/MindboxRemoteMessage.kt +++ b/sdk/src/main/java/cloud/mindbox/mobile_sdk/pushes/MindboxRemoteMessage.kt @@ -4,6 +4,7 @@ import android.app.Activity import android.app.PendingIntent import android.content.Context import android.os.Bundle +import com.google.gson.annotations.SerializedName /** * A class representing mindbox remote message @@ -11,13 +12,13 @@ import android.os.Bundle * with your custom push notification implementation. * */ public data class MindboxRemoteMessage( - val uniqueKey: String, - val title: String, - val description: String, - val pushActions: List, - val pushLink: String?, - val imageUrl: String?, - val payload: String?, + @SerializedName("uniqueKey") val uniqueKey: String, + @SerializedName("title") val title: String, + @SerializedName("description") val description: String, + @SerializedName("pushActions") val pushActions: List, + @SerializedName("pushLink") val pushLink: String?, + @SerializedName("imageUrl") val imageUrl: String?, + @SerializedName("payload") val payload: String?, ) { public companion object { public const val DATA_UNIQUE_KEY: String = "uniqueKey" diff --git a/sdk/src/main/java/cloud/mindbox/mobile_sdk/pushes/PushToken.kt b/sdk/src/main/java/cloud/mindbox/mobile_sdk/pushes/PushToken.kt index d339a096..0ce64511 100644 --- a/sdk/src/main/java/cloud/mindbox/mobile_sdk/pushes/PushToken.kt +++ b/sdk/src/main/java/cloud/mindbox/mobile_sdk/pushes/PushToken.kt @@ -6,6 +6,7 @@ import cloud.mindbox.mobile_sdk.logger.mindboxLogI import cloud.mindbox.mobile_sdk.utils.MindboxUtils.Stopwatch import cloud.mindbox.mobile_sdk.utils.awaitAllWithTimeout import com.google.gson.Gson +import com.google.gson.annotations.SerializedName import com.google.gson.reflect.TypeToken import kotlinx.coroutines.async import kotlinx.coroutines.withContext @@ -18,8 +19,8 @@ internal data class PushToken( ) internal data class PrefPushToken( - val token: String, - val updateDate: Long, + @SerializedName("token") val token: String, + @SerializedName("updateDate") val updateDate: Long, ) internal typealias PushTokenMap = Map diff --git a/settings.gradle b/settings.gradle index 18e56530..0ac91206 100644 --- a/settings.gradle +++ b/settings.gradle @@ -1,4 +1,5 @@ include ':sdk' +include ':detekt-rules' include ':mindbox-huawei' include ':mindbox-firebase' include ':mindbox-rustore'