diff --git a/.editorconfig b/.editorconfig index 215eb9e..1d71e74 100644 --- a/.editorconfig +++ b/.editorconfig @@ -17,4 +17,8 @@ ij_kotlin_name_count_to_use_star_import_for_members = 99999 ij_java_names_count_to_use_import_on_demand = 99999 ktlint_code_style = ktlint_official -ktlint_standard_filename = disabled \ No newline at end of file +ktlint_standard_filename = disabled +ktlint_function_signature_body_expression_wrapping = default +ktlint_standard_multiline-expression-wrapping = disabled +ktlint_standard_string-template-indent = disabled +ktlint_function_signature_rule_force_multiline_when_parameter_count_greater_or_equal_than = 5 \ No newline at end of file diff --git a/.github/workflows/cd.yaml b/.github/workflows/cd.yaml index ea65f3e..980cff3 100644 --- a/.github/workflows/cd.yaml +++ b/.github/workflows/cd.yaml @@ -7,22 +7,35 @@ jobs: build_image: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 - name: Set up JDK 17 - uses: actions/setup-java@v3 + uses: actions/setup-java@v4 with: java-version: '17' distribution: 'temurin' + - name: Cache + uses: actions/cache@v4 + with: + path: | + ./build + ./.gradle + ~/.gradle/caches + ~/.gradle/wrapper + ~/.m2/repository + key: ${{ runner.os }}-gradle-${{ hashFiles('*.gradle.kts') }} + restore-keys: | + ${{ runner.os }}-gradle- + - name: Validate Gradle wrapper uses: gradle/wrapper-validation-action@ccb4328a959376b642e027874838f60f8e596de3 - name: Build Project with Gradle - uses: gradle/gradle-build-action@749f47bda3e44aa060e82d7b3ef7e40d953bd629 + uses: gradle/gradle-build-action@v3 with: arguments: build - - uses: extractions/setup-just@v1 + - uses: extractions/setup-just@v2 - name: Build and publish image with jib run: just build-push-image @@ -34,9 +47,9 @@ jobs: runs-on: ubuntu-latest needs: build_image steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 - - uses: extractions/setup-just@v1 + - uses: extractions/setup-just@v2 - uses: azure/setup-helm@v3 diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index bbee9af..bf90b74 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -7,20 +7,33 @@ jobs: check_verify: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 - name: Set up JDK 17 - uses: actions/setup-java@v3 + uses: actions/setup-java@v4 with: java-version: '17' distribution: 'temurin' cache: gradle + - name: Cache + uses: actions/cache@v4 + with: + path: | + ./build + ./.gradle + ~/.gradle/caches + ~/.gradle/wrapper + ~/.m2/repository + key: ${{ runner.os }}-gradle-${{ hashFiles('*.gradle.kts') }} + restore-keys: | + ${{ runner.os }}-gradle- + - name: Validate Gradle wrapper - uses: gradle/wrapper-validation-action@v1.1.0 + uses: gradle/actions/wrapper-validation@v3 - name: Build Project with Gradle - uses: gradle/gradle-build-action@749f47bda3e44aa060e82d7b3ef7e40d953bd629 + uses: gradle/gradle-build-action@v3 with: arguments: build diff --git a/backend/build.gradle.kts b/backend/build.gradle.kts index 55a095c..bc36f82 100644 --- a/backend/build.gradle.kts +++ b/backend/build.gradle.kts @@ -2,8 +2,9 @@ import com.github.jengelman.gradle.plugins.shadow.tasks.ShadowJar plugins { application - id("com.github.johnrengelman.shadow") version Versions.SHADOW - id("com.google.cloud.tools.jib") version "3.3.2" + alias(libs.plugins.kotlinJvm) + alias(libs.plugins.jib) + alias(libs.plugins.shadow) } group = Artifact.GROUP @@ -37,51 +38,50 @@ jib { dependencies { implementation(kotlin("stdlib")) - implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:${Versions.COROUTINES}") - - implementation(platform("io.vertx:vertx-stack-depchain:${Versions.VERTX}")) - "io.vertx".let { vertx -> - implementation("$vertx:vertx-core") - implementation("$vertx:vertx-web-graphql") - implementation("$vertx:vertx-auth-jwt") - implementation("$vertx:vertx-sql-client-templates") - implementation("$vertx:vertx-web") - implementation("$vertx:vertx-pg-client") - implementation("$vertx:vertx-lang-kotlin-coroutines") - implementation("$vertx:vertx-lang-kotlin") - implementation("$vertx:vertx-health-check") - implementation("$vertx:vertx-web-client") - - testImplementation("$vertx:vertx-junit5") + implementation(libs.kotlinx.coroutines.core) + + implementation(platform(libs.vertx.depchain)) + + with(libs.vertx) { + implementation(core) + implementation(web) + implementation(pgClient) + implementation(coroutines) + implementation(kotlin) + implementation(healthCheck) + implementation(webClient) + + testImplementation(junit5) } - implementation("com.michael-bull.kotlin-result:kotlin-result:${Versions.KOTLIN_RESULT}") + implementation(libs.kotlinResult) - implementation("org.flywaydb:flyway-core:${Versions.FLYWAY}") - implementation("org.postgresql:postgresql:${Versions.POSTGRES}") - implementation("com.ongres.scram:client:${Versions.ONGRESS_SCARM}") + implementation(libs.flyway.core) + implementation(libs.flyway.postgresql) + implementation(libs.postgresql) + implementation(libs.scramOngressClient) - implementation("org.slf4j:slf4j-api:${Versions.SLF4J}") - implementation("org.slf4j:slf4j-simple:${Versions.SLF4J}") - implementation("io.github.microutils:kotlin-logging-jvm:${Versions.JVM_LOGGER}") + implementation(libs.slf4j.api) + implementation(libs.slf4j.simpe) + implementation(libs.kotlinLoggingJvm) - implementation("com.fasterxml.jackson.core:jackson-databind:${Versions.JACKSON}") - implementation("com.fasterxml.jackson.module:jackson-module-kotlin:${Versions.JACKSON}") - implementation("com.fasterxml.jackson.datatype:jackson-datatype-jsr310:${Versions.JACKSON}") + implementation(libs.jackson.databind) + implementation(libs.jackson.moduleKotlin) + implementation(libs.jackson.datatype.jsr310) - implementation("org.kodein.di:kodein-di:${Versions.KODEIN_DI}") + implementation(libs.kodein) + implementation(libs.jobrunr) + implementation(libs.jobrunr.kotlin) + implementation(libs.firebaseAdmin) - implementation("org.jobrunr:jobrunr:${Versions.JOB_RUNNER}") - implementation("org.jobrunr:jobrunr-kotlin-1.8-support:${Versions.JOB_RUNNER}") - implementation("com.google.firebase:firebase-admin:9.2.0") + testImplementation(libs.junit.jupiter) + testImplementation(libs.kotest.assertions.core) + testImplementation(libs.mockk) - testImplementation("org.junit.jupiter:junit-jupiter:${Versions.JUNIT_JUPITER}") - testImplementation("io.kotest:kotest-assertions-core:${Versions.KO_TEST}") - testImplementation("io.mockk:mockk:${Versions.MOCKK}") - with("org.testcontainers") { - testImplementation("$this:testcontainers:${Versions.TEST_CONTAINERS}") - testImplementation("$this:junit-jupiter:${Versions.TEST_CONTAINERS}") - testImplementation("$this:postgresql:${Versions.TEST_CONTAINERS}") + with(libs.testcontainers) { + testImplementation(this) + testImplementation(junit) + testImplementation(postgresql) } } @@ -95,13 +95,14 @@ tasks.withType { } tasks.withType { - args = listOf( - "run", - mainVerticleName, - "--redeploy=$watchForChange", - "--launcher-class=$launcherClassName", - "--on-redeploy=$doOnChange" - ) + args = + listOf( + "run", + mainVerticleName, + "--redeploy=$watchForChange", + "--launcher-class=$launcherClassName", + "--on-redeploy=$doOnChange", + ) } tasks.test { diff --git a/backend/src/main/kotlin/me/sujanpoudel/playdeals/Conf.kt b/backend/src/main/kotlin/me/sujanpoudel/playdeals/Conf.kt index b745318..0c9f381 100644 --- a/backend/src/main/kotlin/me/sujanpoudel/playdeals/Conf.kt +++ b/backend/src/main/kotlin/me/sujanpoudel/playdeals/Conf.kt @@ -3,7 +3,7 @@ package me.sujanpoudel.playdeals enum class Environment { PRODUCTION, DEVELOPMENT, - TEST + TEST, } data class Conf( @@ -12,7 +12,7 @@ data class Conf( val environment: Environment, val backgroundTask: BackgroundTask, val firebaseAuthCredential: String, - val forexApiKey: String + val forexApiKey: String, ) { data class DB( val host: String, @@ -20,13 +20,13 @@ data class Conf( val name: String, val username: String, val password: String, - val poolSize: Int + val poolSize: Int, ) data class BackgroundTask( val dashboardEnabled: Boolean, val dashboardUserName: String, - val dashboardPassword: String + val dashboardPassword: String, ) data class Api(val port: Int, val cors: String) diff --git a/backend/src/main/kotlin/me/sujanpoudel/playdeals/DIConfigurer.kt b/backend/src/main/kotlin/me/sujanpoudel/playdeals/DIConfigurer.kt index 8e36e27..99e799c 100644 --- a/backend/src/main/kotlin/me/sujanpoudel/playdeals/DIConfigurer.kt +++ b/backend/src/main/kotlin/me/sujanpoudel/playdeals/DIConfigurer.kt @@ -12,8 +12,8 @@ import com.google.firebase.messaging.FirebaseMessaging import io.vertx.core.Vertx import io.vertx.core.eventbus.DeliveryOptions import io.vertx.core.json.jackson.DatabindCodec +import io.vertx.pgclient.PgBuilder import io.vertx.pgclient.PgConnectOptions -import io.vertx.pgclient.PgPool import io.vertx.sqlclient.PoolOptions import me.sujanpoudel.playdeals.api.ApiVerticle import me.sujanpoudel.playdeals.jobs.AndroidAppExpiryCheckScheduler @@ -36,6 +36,7 @@ import org.flywaydb.core.Flyway import org.jobrunr.configuration.JobRunr import org.jobrunr.configuration.JobRunrConfiguration import org.jobrunr.dashboard.JobRunrDashboardWebServerConfiguration +import org.jobrunr.jobs.filters.RetryFilter import org.jobrunr.server.BackgroundJobServerConfiguration import org.jobrunr.server.JobActivator import org.jobrunr.storage.StorageProvider @@ -50,10 +51,7 @@ import java.time.Duration inline fun DI.get(tag: String? = null) = direct.instance(tag) -fun configureDI( - vertx: Vertx, - conf: Conf -) = DI { +fun configureDI(vertx: Vertx, conf: Conf) = DI { bindSingleton { conf } bindSingleton { ApiVerticle(di = this) } @@ -67,7 +65,7 @@ fun configureDI( MainVerticle( apiVerticle = instance(), backgroundJobsVerticle = instance(), - flywayVerticle = instance() + flywayVerticle = instance(), ) } @@ -93,11 +91,12 @@ fun configureDI( } bindSingleton { - PgPool.client(vertx, instance(), PoolOptions().setMaxSize(conf.db.poolSize)) - } - - bindSingleton { - PgPool.pool(vertx, instance(), PoolOptions()) + PgBuilder + .client() + .using(vertx) + .connectingTo(instance()) + .with(PoolOptions().setMaxSize(conf.db.poolSize)) + .build() } bindSingleton { @@ -114,26 +113,27 @@ fun configureDI( setURL("jdbc:postgresql://${conf.db.host}:${conf.db.port}/${conf.db.name}?currentSchema=job_runr") user = conf.db.username password = conf.db.password - } + }, ) } bindSingleton { JobRunr.configure() .useStorageProvider(instance()) + .withJobFilter(RetryFilter(2)) .useDashboardIf( conf.backgroundTask.dashboardEnabled, JobRunrDashboardWebServerConfiguration .usingStandardDashboardConfiguration() - .andBasicAuthentication(conf.backgroundTask.dashboardUserName, conf.backgroundTask.dashboardPassword) + .andBasicAuthentication(conf.backgroundTask.dashboardUserName, conf.backgroundTask.dashboardPassword), ) .useJobActivator(instance()) .useBackgroundJobServer( BackgroundJobServerConfiguration.usingStandardBackgroundJobServerConfiguration() .andDeleteSucceededJobsAfter(Duration.ofMinutes(10)) .andPermanentlyDeleteDeletedJobsAfter(Duration.ofMinutes(10)) - .andWorkerCount(1) - .andPollIntervalInSeconds(10) + .andWorkerCount(2) + .andPollIntervalInSeconds(10), ) .initialize() } @@ -157,13 +157,13 @@ fun configureDI( AndroidAppExpiryCheckScheduler( repository = instance(), requestScheduler = instance(), - storageProvider = instance() + storageProvider = instance(), ) } bindSingleton { ForexFetcher( di = di, - conf = instance() + conf = instance(), ) } diff --git a/backend/src/main/kotlin/me/sujanpoudel/playdeals/FlywayVerticle.kt b/backend/src/main/kotlin/me/sujanpoudel/playdeals/FlywayVerticle.kt index 6d290d7..b1be991 100644 --- a/backend/src/main/kotlin/me/sujanpoudel/playdeals/FlywayVerticle.kt +++ b/backend/src/main/kotlin/me/sujanpoudel/playdeals/FlywayVerticle.kt @@ -4,7 +4,6 @@ import io.vertx.kotlin.coroutines.CoroutineVerticle import org.flywaydb.core.Flyway class FlywayVerticle(private val flyway: Flyway) : CoroutineVerticle() { - override suspend fun start() { flyway.migrate() } diff --git a/backend/src/main/kotlin/me/sujanpoudel/playdeals/Main.kt b/backend/src/main/kotlin/me/sujanpoudel/playdeals/Main.kt index 8f09984..198339d 100644 --- a/backend/src/main/kotlin/me/sujanpoudel/playdeals/Main.kt +++ b/backend/src/main/kotlin/me/sujanpoudel/playdeals/Main.kt @@ -3,7 +3,7 @@ package me.sujanpoudel.playdeals import com.fasterxml.jackson.databind.ObjectMapper import com.github.michaelbull.result.getOrThrow import io.vertx.core.Vertx -import io.vertx.kotlin.coroutines.await +import io.vertx.kotlin.coroutines.coAwait import kotlinx.coroutines.runBlocking import me.sujanpoudel.playdeals.common.BootstrapException import me.sujanpoudel.playdeals.common.buildConf @@ -27,5 +27,5 @@ fun main(): Unit = runBlocking { .onFailure { logger.error(it) { "Error deploying main verticle" } vertx.close() - }.await() + }.coAwait() } diff --git a/backend/src/main/kotlin/me/sujanpoudel/playdeals/MainVerticle.kt b/backend/src/main/kotlin/me/sujanpoudel/playdeals/MainVerticle.kt index 7fb7237..7f0542b 100644 --- a/backend/src/main/kotlin/me/sujanpoudel/playdeals/MainVerticle.kt +++ b/backend/src/main/kotlin/me/sujanpoudel/playdeals/MainVerticle.kt @@ -1,19 +1,18 @@ package me.sujanpoudel.playdeals import io.vertx.kotlin.coroutines.CoroutineVerticle -import io.vertx.kotlin.coroutines.await +import io.vertx.kotlin.coroutines.coAwait import me.sujanpoudel.playdeals.api.ApiVerticle import me.sujanpoudel.playdeals.jobs.BackgroundJobsVerticle class MainVerticle( private val apiVerticle: ApiVerticle, private val flywayVerticle: FlywayVerticle, - private val backgroundJobsVerticle: BackgroundJobsVerticle + private val backgroundJobsVerticle: BackgroundJobsVerticle, ) : CoroutineVerticle() { - override suspend fun start() { - vertx.deployVerticle(flywayVerticle).await() - vertx.deployVerticle(backgroundJobsVerticle).await() - vertx.deployVerticle(apiVerticle).await() + vertx.deployVerticle(flywayVerticle).coAwait() + vertx.deployVerticle(backgroundJobsVerticle).coAwait() + vertx.deployVerticle(apiVerticle).coAwait() } } diff --git a/backend/src/main/kotlin/me/sujanpoudel/playdeals/api/ApiVerticle.kt b/backend/src/main/kotlin/me/sujanpoudel/playdeals/api/ApiVerticle.kt index 36e6a9a..4faa7ad 100644 --- a/backend/src/main/kotlin/me/sujanpoudel/playdeals/api/ApiVerticle.kt +++ b/backend/src/main/kotlin/me/sujanpoudel/playdeals/api/ApiVerticle.kt @@ -3,7 +3,7 @@ package me.sujanpoudel.playdeals.api import io.vertx.ext.web.Router import io.vertx.ext.web.handler.CorsHandler import io.vertx.kotlin.coroutines.CoroutineVerticle -import io.vertx.kotlin.coroutines.await +import io.vertx.kotlin.coroutines.coAwait import me.sujanpoudel.playdeals.Conf import me.sujanpoudel.playdeals.api.deals.appDealsApi import me.sujanpoudel.playdeals.logger @@ -11,9 +11,8 @@ import org.kodein.di.DirectDI import org.kodein.di.instance class ApiVerticle( - private val di: DirectDI + private val di: DirectDI, ) : CoroutineVerticle() { - override suspend fun start() { val config = di.instance() val router = Router.router(vertx) @@ -26,7 +25,7 @@ class ApiVerticle( vertx.createHttpServer() .requestHandler(router) .listen(config.api.port) - .await() + .coAwait() logger.info("API server running at : ${config.api.port}") } diff --git a/backend/src/main/kotlin/me/sujanpoudel/playdeals/api/ForexRate.kt b/backend/src/main/kotlin/me/sujanpoudel/playdeals/api/ForexRate.kt index 91bf83f..c61bf41 100644 --- a/backend/src/main/kotlin/me/sujanpoudel/playdeals/api/ForexRate.kt +++ b/backend/src/main/kotlin/me/sujanpoudel/playdeals/api/ForexRate.kt @@ -8,16 +8,13 @@ import me.sujanpoudel.playdeals.usecases.executeUseCase import org.kodein.di.DirectDI import org.kodein.di.instance -fun forexRateApi( - di: DirectDI, - vertx: io.vertx.core.Vertx -): Router = Router.router(vertx).apply { +fun forexRateApi(di: DirectDI, vertx: io.vertx.core.Vertx): Router = Router.router(vertx).apply { get() .coHandler { ctx -> ctx.executeUseCase( useCase = di.instance(), toContext = { }, - toInput = { } + toInput = { }, ) { ctx.json(jsonResponse(data = it)) } diff --git a/backend/src/main/kotlin/me/sujanpoudel/playdeals/api/Health.kt b/backend/src/main/kotlin/me/sujanpoudel/playdeals/api/Health.kt index 4136cc4..375195f 100644 --- a/backend/src/main/kotlin/me/sujanpoudel/playdeals/api/Health.kt +++ b/backend/src/main/kotlin/me/sujanpoudel/playdeals/api/Health.kt @@ -12,10 +12,7 @@ import me.sujanpoudel.playdeals.usecases.DBHealthUseCase import org.kodein.di.DirectDI import org.kodein.di.instance -fun healthApi( - di: DirectDI, - vertx: Vertx -): Router = Router.router(vertx).apply { +fun healthApi(di: DirectDI, vertx: Vertx): Router = Router.router(vertx).apply { val dbHealthChecker = di.instance() val livenessHandler = HealthCheckHandler.create(vertx) diff --git a/backend/src/main/kotlin/me/sujanpoudel/playdeals/api/deals/Api.kt b/backend/src/main/kotlin/me/sujanpoudel/playdeals/api/deals/Api.kt index 196a0e3..b44a586 100644 --- a/backend/src/main/kotlin/me/sujanpoudel/playdeals/api/deals/Api.kt +++ b/backend/src/main/kotlin/me/sujanpoudel/playdeals/api/deals/Api.kt @@ -2,7 +2,7 @@ package me.sujanpoudel.playdeals.api.deals import io.vertx.core.Vertx import io.vertx.ext.web.Router -import io.vertx.kotlin.coroutines.await +import io.vertx.kotlin.coroutines.coAwait import me.sujanpoudel.playdeals.ContentTypes import me.sujanpoudel.playdeals.common.coHandler import me.sujanpoudel.playdeals.common.jsonResponse @@ -12,16 +12,13 @@ import me.sujanpoudel.playdeals.usecases.executeUseCase import org.kodein.di.DirectDI import org.kodein.di.instance -fun appDealsApi( - di: DirectDI, - vertx: Vertx -): Router = Router.router(vertx).apply { +fun appDealsApi(di: DirectDI, vertx: Vertx): Router = Router.router(vertx).apply { get() .coHandler { ctx -> ctx.executeUseCase( useCase = di.instance(), toContext = { GetDealsContext(ctx.request().params()) }, - toInput = { GetDealsUseCase.Input(it.skip, it.take) } + toInput = { GetDealsUseCase.Input(it.skip, it.take) }, ) { ctx.json(jsonResponse(data = it)) } @@ -32,8 +29,8 @@ fun appDealsApi( .coHandler { ctx -> ctx.executeUseCase( useCase = di.instance(), - toContext = { NewDealContext(ctx.request().body().await().toJsonObject()) }, - toInput = { it.packageName } + toContext = { NewDealContext(ctx.request().body().coAwait().toJsonObject()) }, + toInput = { it.packageName }, ) { ctx.json(jsonResponse("App added for queue")) } diff --git a/backend/src/main/kotlin/me/sujanpoudel/playdeals/api/deals/GetDealsContext.kt b/backend/src/main/kotlin/me/sujanpoudel/playdeals/api/deals/GetDealsContext.kt index 65d5f52..1ebb283 100644 --- a/backend/src/main/kotlin/me/sujanpoudel/playdeals/api/deals/GetDealsContext.kt +++ b/backend/src/main/kotlin/me/sujanpoudel/playdeals/api/deals/GetDealsContext.kt @@ -5,9 +5,8 @@ import me.sujanpoudel.playdeals.exceptions.ClientErrorException import me.sujanpoudel.playdeals.usecases.Validated class GetDealsContext( - private val param: MultiMap + private val param: MultiMap, ) : Validated { - val skip by lazy { param.get("skip")?.toIntOrNull() ?: 0 } val take by lazy { param.get("take")?.toIntOrNull() ?: 10 } diff --git a/backend/src/main/kotlin/me/sujanpoudel/playdeals/api/deals/NewDealContext.kt b/backend/src/main/kotlin/me/sujanpoudel/playdeals/api/deals/NewDealContext.kt index 0442060..cff8c9c 100644 --- a/backend/src/main/kotlin/me/sujanpoudel/playdeals/api/deals/NewDealContext.kt +++ b/backend/src/main/kotlin/me/sujanpoudel/playdeals/api/deals/NewDealContext.kt @@ -6,9 +6,8 @@ import me.sujanpoudel.playdeals.exceptions.ClientErrorException import me.sujanpoudel.playdeals.usecases.Validated class NewDealContext( - private val request: JsonObject + private val request: JsonObject, ) : Validated { - private val packageNameField = "packageName" val packageName: String by lazy { request.getString(packageNameField) } diff --git a/backend/src/main/kotlin/me/sujanpoudel/playdeals/common/Conf.kt b/backend/src/main/kotlin/me/sujanpoudel/playdeals/common/Conf.kt index d0279ab..089cc2d 100644 --- a/backend/src/main/kotlin/me/sujanpoudel/playdeals/common/Conf.kt +++ b/backend/src/main/kotlin/me/sujanpoudel/playdeals/common/Conf.kt @@ -13,16 +13,12 @@ fun buildConf(envs: Map) = com.github.michaelbull.result.runCatc val violations = mutableListOf() @Suppress("UNCHECKED_CAST") - fun env( - envVarName: String, - default: String? = null, - converter: (String) -> T? = { it as? T } - ): T? = ( + fun env(envVarName: String, default: String? = null, converter: (String) -> T? = { it as? T }): T? = ( envs[envVarName] ?: default ?: run { violations += "No '$envVarName' env var defined!".also { logger.error { it } } null } - )?.let(converter) ?: run { + )?.let(converter) ?: run { violations += "Invalid '$envVarName'" null } @@ -43,9 +39,10 @@ fun buildConf(envs: Map) = com.github.michaelbull.result.runCatc val dashboardUser = env("DASHBOARD_USER", "admin") val dashboardPassword = env("DASHBOARD_PASS", "admin") - val firebaseAuthCredential = env("FIREBASE_ADMIN_AUTH_CREDENTIALS") { - Base64.getDecoder().decode(it).decodeToString() - } + val firebaseAuthCredential = + env("FIREBASE_ADMIN_AUTH_CREDENTIALS") { + Base64.getDecoder().decode(it).decodeToString() + } val forexApiKey = env("FOREX_API_KEY") @@ -55,21 +52,23 @@ fun buildConf(envs: Map) = com.github.michaelbull.result.runCatc Conf( api = Conf.Api(appPort!!, cors = cors!!), environment = environment!!, - db = Conf.DB( - host = dbHost!!, - port = dbPort!!, - name = dbName!!, - username = dbUsername!!, - password = dbPassword!!, - poolSize = dbPoolSize!! - ), - backgroundTask = Conf.BackgroundTask( - dashboardEnabled = dashboardEnabled!!, - dashboardUserName = dashboardUser!!, - dashboardPassword = dashboardPassword!! - ), + db = + Conf.DB( + host = dbHost!!, + port = dbPort!!, + name = dbName!!, + username = dbUsername!!, + password = dbPassword!!, + poolSize = dbPoolSize!!, + ), + backgroundTask = + Conf.BackgroundTask( + dashboardEnabled = dashboardEnabled!!, + dashboardUserName = dashboardUser!!, + dashboardPassword = dashboardPassword!!, + ), firebaseAuthCredential = firebaseAuthCredential!!, - forexApiKey = forexApiKey!! + forexApiKey = forexApiKey!!, ) } } diff --git a/backend/src/main/kotlin/me/sujanpoudel/playdeals/common/Enums.kt b/backend/src/main/kotlin/me/sujanpoudel/playdeals/common/Enums.kt index a6ba8e8..c52cd92 100644 --- a/backend/src/main/kotlin/me/sujanpoudel/playdeals/common/Enums.kt +++ b/backend/src/main/kotlin/me/sujanpoudel/playdeals/common/Enums.kt @@ -1,6 +1,7 @@ package me.sujanpoudel.playdeals.common inline fun > String.asEnum() = enumValueOf(this) + inline fun > String.asEnumOrNull() = try { asEnum() } catch (e: Exception) { diff --git a/backend/src/main/kotlin/me/sujanpoudel/playdeals/common/Metrices.kt b/backend/src/main/kotlin/me/sujanpoudel/playdeals/common/Metrices.kt index 52a3dd5..5471ea9 100644 --- a/backend/src/main/kotlin/me/sujanpoudel/playdeals/common/Metrices.kt +++ b/backend/src/main/kotlin/me/sujanpoudel/playdeals/common/Metrices.kt @@ -1,14 +1,13 @@ package me.sujanpoudel.playdeals.common -import me.sujanpoudel.playdeals.logger +import me.sujanpoudel.playdeals.jobs.info +import org.jobrunr.jobs.lambdas.JobRequestHandler import kotlin.time.DurationUnit import kotlin.time.measureTimedValue -inline fun loggingExecutionTime(message: String, action: () -> T): T { - val timedValue = measureTimedValue { - action.invoke() - } - logger.info("$message (took ${timedValue.duration.toString(DurationUnit.MILLISECONDS)})") +inline fun JobRequestHandler<*>.loggingExecutionTime(message: String, action: () -> T): T { + val timedValue = measureTimedValue { action.invoke() } + info("$message took ${timedValue.duration.toString(DurationUnit.MILLISECONDS)}ms") return timedValue.value } diff --git a/backend/src/main/kotlin/me/sujanpoudel/playdeals/common/Routing.kt b/backend/src/main/kotlin/me/sujanpoudel/playdeals/common/Routing.kt index 48c81d1..3951e3d 100644 --- a/backend/src/main/kotlin/me/sujanpoudel/playdeals/common/Routing.kt +++ b/backend/src/main/kotlin/me/sujanpoudel/playdeals/common/Routing.kt @@ -27,21 +27,19 @@ fun Route.coHandler(fn: suspend (RoutingContext) -> Unit): Route { fun HttpServerResponse.contentType(value: String): HttpServerResponse = putHeader("Content-Type", value) -fun jsonResponse( - message: String = "Success", - data: T? = null -): JsonObject = jsonObjectOf( +fun jsonResponse(message: String = "Success", data: T? = null): JsonObject = jsonObjectOf( "message" to message, - "data" to data + "data" to data, ) const val UNKNOWN_ERROR_MESSAGE = "Something went wrong" fun RoutingContext.handleExceptions(exception: Throwable) { - val (message, statusCode) = when (exception) { - is ClientErrorException -> exception.message to exception.statusCode - else -> UNKNOWN_ERROR_MESSAGE to 500 - } + val (message, statusCode) = + when (exception) { + is ClientErrorException -> exception.message to exception.statusCode + else -> UNKNOWN_ERROR_MESSAGE to 500 + } this.response() .setStatusCode(statusCode) diff --git a/backend/src/main/kotlin/me/sujanpoudel/playdeals/domain/AndroidAppDetail.kt b/backend/src/main/kotlin/me/sujanpoudel/playdeals/domain/AndroidAppDetail.kt index 57e86f9..eefab6a 100644 --- a/backend/src/main/kotlin/me/sujanpoudel/playdeals/domain/AndroidAppDetail.kt +++ b/backend/src/main/kotlin/me/sujanpoudel/playdeals/domain/AndroidAppDetail.kt @@ -16,7 +16,7 @@ data class AndroidAppDetail( val downloads: String, val rating: String, val offerExpiresIn: OffsetDateTime?, - val source: String + val source: String, ) fun AndroidAppDetail.asNewDeal() = NewDeal( @@ -33,5 +33,5 @@ fun AndroidAppDetail.asNewDeal() = NewDeal( rating = rating, offerExpiresIn = offerExpiresIn!!, type = DealType.ANDROID_APP, - source = source + source = source, ) diff --git a/backend/src/main/kotlin/me/sujanpoudel/playdeals/domain/ForexRate.kt b/backend/src/main/kotlin/me/sujanpoudel/playdeals/domain/ForexRate.kt index c85f694..eed6663 100644 --- a/backend/src/main/kotlin/me/sujanpoudel/playdeals/domain/ForexRate.kt +++ b/backend/src/main/kotlin/me/sujanpoudel/playdeals/domain/ForexRate.kt @@ -5,12 +5,12 @@ import java.time.OffsetDateTime // Rates are USD based data class ForexRate( val timestamp: OffsetDateTime, - val rates: List + val rates: List, ) data class ConversionRate( val currency: String, val symbol: String, val name: String, - val rate: Float + val rate: Float, ) diff --git a/backend/src/main/kotlin/me/sujanpoudel/playdeals/domain/NewDeal.kt b/backend/src/main/kotlin/me/sujanpoudel/playdeals/domain/NewDeal.kt index 5ecc8b2..a2b82dd 100644 --- a/backend/src/main/kotlin/me/sujanpoudel/playdeals/domain/NewDeal.kt +++ b/backend/src/main/kotlin/me/sujanpoudel/playdeals/domain/NewDeal.kt @@ -17,23 +17,24 @@ data class NewDeal( val rating: String, val offerExpiresIn: OffsetDateTime, val type: DealType, - val source: String + val source: String, ) val NewDeal.insertValues - get() = arrayOf( - id, - name, - icon, - images.toTypedArray(), - normalPrice, - currentPrice, - currency, - storeUrl, - category, - downloads, - rating, - offerExpiresIn, - type.toString(), - source - ) + get() = + arrayOf( + id, + name, + icon, + images.toTypedArray(), + normalPrice, + currentPrice, + currency, + storeUrl, + category, + downloads, + rating, + offerExpiresIn, + type.toString(), + source, + ) diff --git a/backend/src/main/kotlin/me/sujanpoudel/playdeals/domain/entities/DealEntity.kt b/backend/src/main/kotlin/me/sujanpoudel/playdeals/domain/entities/DealEntity.kt index e4f4625..20b5411 100644 --- a/backend/src/main/kotlin/me/sujanpoudel/playdeals/domain/entities/DealEntity.kt +++ b/backend/src/main/kotlin/me/sujanpoudel/playdeals/domain/entities/DealEntity.kt @@ -7,7 +7,7 @@ enum class DealType { ANDROID_APP, IOS_APP, DESKTOP_APP, - OTHER + OTHER, } data class DealEntity( @@ -26,7 +26,7 @@ data class DealEntity( val type: DealType, val source: String, val createdAt: OffsetDateTime, - val updatedAt: OffsetDateTime + val updatedAt: OffsetDateTime, ) private fun String.asCurrencySymbol() = when (this) { @@ -38,11 +38,12 @@ private fun Float.formatAsPrice(): String { val int = toInt() val decimal = ((this - int) * 100).roundToInt() - val formattedDecimal = if (decimal < 10) { - "${decimal}0" - } else { - "$decimal" - } + val formattedDecimal = + if (decimal < 10) { + "${decimal}0" + } else { + "$decimal" + } return "$int.$formattedDecimal" } diff --git a/backend/src/main/kotlin/me/sujanpoudel/playdeals/domain/entities/Mappers.kt b/backend/src/main/kotlin/me/sujanpoudel/playdeals/domain/entities/Mappers.kt index 8e1b44b..82f1c91 100644 --- a/backend/src/main/kotlin/me/sujanpoudel/playdeals/domain/entities/Mappers.kt +++ b/backend/src/main/kotlin/me/sujanpoudel/playdeals/domain/entities/Mappers.kt @@ -20,11 +20,11 @@ fun Row.asAppDeal(): DealEntity { offerExpiresIn = get("offer_expires_in"), type = getString("type").asEnum(), source = get("source"), - createdAt = get("created_at"), - updatedAt = get("updated_at") + updatedAt = get("updated_at"), ) } fun Row?.valueOrNull() = this?.getString("value") + fun Row.value(): String = getString("value") diff --git a/backend/src/main/kotlin/me/sujanpoudel/playdeals/jobs/AndroidAppDetailScrapper.kt b/backend/src/main/kotlin/me/sujanpoudel/playdeals/jobs/AndroidAppDetailScrapper.kt index d4bb8c0..aaee7de 100644 --- a/backend/src/main/kotlin/me/sujanpoudel/playdeals/jobs/AndroidAppDetailScrapper.kt +++ b/backend/src/main/kotlin/me/sujanpoudel/playdeals/jobs/AndroidAppDetailScrapper.kt @@ -2,12 +2,16 @@ package me.sujanpoudel.playdeals.jobs import com.fasterxml.jackson.core.JsonParser import com.fasterxml.jackson.databind.ObjectMapper +import com.github.michaelbull.result.Result +import com.github.michaelbull.result.getOrElse +import com.github.michaelbull.result.runCatching +import io.vertx.core.json.Json import io.vertx.core.json.JsonArray import io.vertx.core.json.JsonObject import io.vertx.ext.web.client.WebClient import io.vertx.ext.web.client.WebClientOptions import io.vertx.kotlin.core.json.jsonObjectOf -import io.vertx.kotlin.coroutines.await +import io.vertx.kotlin.coroutines.coAwait import me.sujanpoudel.playdeals.Constants import me.sujanpoudel.playdeals.common.SIMPLE_NAME import me.sujanpoudel.playdeals.common.loggingExecutionTime @@ -38,13 +42,12 @@ enum class Value(val root: String, vararg val path: Int) { CURRENCY("ds:5", 1, 2, 57, 0, 0, 0, 0, 1, 0, 1), GENRE("ds:5", 1, 2, 79, 0, 0, 0), SCREENSHOTS_LIST("ds:5", 1, 2, 78, 0), - SCREENSHOTS_URL("", 3, 2); + SCREENSHOTS_URL("", 3, 2), } class AppDetailScrapper( - override val di: DI + override val di: DI, ) : CoJobRequestHandler(), DIAware { - private val jobsVerticle by instance() private val repository by instance() private val messagingService by instance() @@ -53,12 +56,14 @@ class AppDetailScrapper( } override suspend fun handleRequest(jobRequest: Request): Unit = loggingExecutionTime( - "$SIMPLE_NAME:: handleRequest ${jobRequest.packageName}" + "$SIMPLE_NAME:: handleRequest ${jobRequest.packageName}", ) { val packageName = jobRequest.packageName val app = loggingExecutionTime("$SIMPLE_NAME:: scrapping app details $packageName") { getAppDetail(packageName) + }.getOrElse { + throw RuntimeException("AppDetailScrapper failed to scrap details ${it.message}") } when { @@ -68,12 +73,12 @@ class AppDetailScrapper( } app.normalPrice == app.currentPrice -> { - logger.infoNotify("App $packageName(${app.name}) deals has been expired") + infoNotify("App $packageName(${app.name}) deals has been expired") repository.delete(packageName) } (app.currentPrice ?: 0f) < app.normalPrice -> { - logger.info("Found deal for $packageName(${app.name}) ${app.currentPrice} ${app.currency}(${app.normalPrice} ${app.currency})") + info("Found deal for $packageName(${app.name}) ${app.currentPrice} ${app.currency}(${app.normalPrice} ${app.currency})") repository.upsert(app.asNewDeal()).also { messagingService.sendMessageForNewDeal(it) } @@ -81,10 +86,10 @@ class AppDetailScrapper( } } - private suspend fun getAppDetail(packageName: String): AndroidAppDetail { + private suspend fun getAppDetail(packageName: String): Result = runCatching { val response = webClient.get("/store/apps/details?id=$packageName&hl=en&gl=us") .send() - .await() + .coAwait() val body = response.bodyAsString() @@ -100,7 +105,7 @@ class AppDetailScrapper( } snippets }.map { - io.vertx.core.json.Json.decodeValue(mapper.readTree(it).toPrettyString()) as JsonObject + Json.decodeValue(mapper.readTree(it).toPrettyString()) as JsonObject } val combined = jsonObjectOf() @@ -112,13 +117,14 @@ class AppDetailScrapper( val currentPrice = combined.getValue(Value.CURRENT_PRICE) / PRICE_MULTIPLIER val normalPrice = combined.getValueOrNull(Value.NORMAL_PRICE)?.div(PRICE_MULTIPLIER) ?: currentPrice - return AndroidAppDetail( + AndroidAppDetail( id = packageName, name = combined.getValue(Value.TITLE), icon = combined.getValue(Value.ICON), - images = (combined.getValue(Value.SCREENSHOTS_LIST) as JsonArray).mapNotNull { - getValue(it as JsonArray, Value.SCREENSHOTS_URL.path.toTypedArray()) as? String - }, + images = + (combined.getValue(Value.SCREENSHOTS_LIST) as JsonArray).mapNotNull { + getValue(it as JsonArray, Value.SCREENSHOTS_URL.path.toTypedArray()) as? String + }, normalPrice = normalPrice, currency = combined.getValue(Value.CURRENCY) as String, currentPrice = currentPrice, @@ -126,10 +132,11 @@ class AppDetailScrapper( downloads = combined.getValue(Value.INSTALLS), storeUrl = "https://play.google.com/store/apps/details?id=$packageName", category = combined.getValue(Value.GENRE) as String, - offerExpiresIn = combined.getValueOrNull(Value.OFFER_END_TIME)?.let { - OffsetDateTime.ofInstant(Instant.ofEpochSecond(it.toLong()), ZoneOffset.UTC) - }, - source = Constants.DealSources.APP_DEAL_SUBREDDIT + offerExpiresIn = + combined.getValueOrNull(Value.OFFER_END_TIME)?.let { + OffsetDateTime.ofInstant(Instant.ofEpochSecond(it.toLong()), ZoneOffset.UTC) + }, + source = Constants.DealSources.APP_DEAL_SUBREDDIT, ) } diff --git a/backend/src/main/kotlin/me/sujanpoudel/playdeals/jobs/AndroidAppExpiryCheckScheduler.kt b/backend/src/main/kotlin/me/sujanpoudel/playdeals/jobs/AndroidAppExpiryCheckScheduler.kt index c533145..6f41606 100644 --- a/backend/src/main/kotlin/me/sujanpoudel/playdeals/jobs/AndroidAppExpiryCheckScheduler.kt +++ b/backend/src/main/kotlin/me/sujanpoudel/playdeals/jobs/AndroidAppExpiryCheckScheduler.kt @@ -2,7 +2,6 @@ package me.sujanpoudel.playdeals.jobs import me.sujanpoudel.playdeals.common.SIMPLE_NAME import me.sujanpoudel.playdeals.common.loggingExecutionTime -import me.sujanpoudel.playdeals.logger import me.sujanpoudel.playdeals.repositories.DealRepository import org.jobrunr.jobs.lambdas.JobRequest import org.jobrunr.jobs.states.StateName @@ -17,20 +16,20 @@ import java.util.UUID class AndroidAppExpiryCheckScheduler( private val repository: DealRepository, private val requestScheduler: JobRequestScheduler, - private val storageProvider: StorageProvider + private val storageProvider: StorageProvider, ) : CoJobRequestHandler() { - override suspend fun handleRequest(jobRequest: Request): Unit = loggingExecutionTime( - "$SIMPLE_NAME:: handleRequest" + "$SIMPLE_NAME:: handleRequest", ) { - val apps = repository.getPotentiallyExpiredDeals().stream() - .map { AppDetailScrapper.Request(it.id) } + val apps = + repository.getPotentiallyExpiredDeals().stream() + .map { AppDetailScrapper.Request(it.id) } requestScheduler.enqueue(apps) val lastUpdatedTime = Instant.now().minus(1, ChronoUnit.HOURS) val jobs = storageProvider.deleteJobsPermanently(StateName.FAILED, lastUpdatedTime) - logger.info("deleted FAILED `$jobs`") + info("deleted FAILED `$jobs`") } class Request private constructor() : JobRequest { @@ -38,6 +37,7 @@ class AndroidAppExpiryCheckScheduler( companion object { private val JOB_ID: UUID = UUID.nameUUIDFromBytes("AppExpiryCheckScheduler".toByteArray()) + operator fun invoke(): RecurringJobBuilder = RecurringJobBuilder.aRecurringJob() .withJobRequest(Request()) .withName("App Expiry Checker") diff --git a/backend/src/main/kotlin/me/sujanpoudel/playdeals/jobs/BackgroundJobsVerticle.kt b/backend/src/main/kotlin/me/sujanpoudel/playdeals/jobs/BackgroundJobsVerticle.kt index 2b46299..4eb1205 100644 --- a/backend/src/main/kotlin/me/sujanpoudel/playdeals/jobs/BackgroundJobsVerticle.kt +++ b/backend/src/main/kotlin/me/sujanpoudel/playdeals/jobs/BackgroundJobsVerticle.kt @@ -11,9 +11,8 @@ import org.kodein.di.direct import org.kodein.di.instance class BackgroundJobsVerticle( - override val di: DI + override val di: DI, ) : CoroutineVerticle(), DIAware { - private val jobRequestScheduler by instance() private val keyValuesRepository by instance() diff --git a/backend/src/main/kotlin/me/sujanpoudel/playdeals/jobs/CoJobRequestHandler.kt b/backend/src/main/kotlin/me/sujanpoudel/playdeals/jobs/CoJobRequestHandler.kt index ac0608b..6a38e7a 100644 --- a/backend/src/main/kotlin/me/sujanpoudel/playdeals/jobs/CoJobRequestHandler.kt +++ b/backend/src/main/kotlin/me/sujanpoudel/playdeals/jobs/CoJobRequestHandler.kt @@ -1,6 +1,8 @@ package me.sujanpoudel.playdeals.jobs import kotlinx.coroutines.runBlocking +import me.sujanpoudel.playdeals.infoNotify +import me.sujanpoudel.playdeals.logger import org.jobrunr.jobs.lambdas.JobRequest import org.jobrunr.jobs.lambdas.JobRequestHandler @@ -11,3 +13,13 @@ abstract class CoJobRequestHandler : JobRequestHandler { abstract suspend fun handleRequest(jobRequest: T) } + +fun JobRequestHandler<*>.info(message: String) { + jobContext().logger().info(message) + logger.info(message) +} + +fun JobRequestHandler<*>.infoNotify(message: String) { + jobContext().logger().info(message) + logger.infoNotify(message) +} diff --git a/backend/src/main/kotlin/me/sujanpoudel/playdeals/jobs/DealSummarizer.kt b/backend/src/main/kotlin/me/sujanpoudel/playdeals/jobs/DealSummarizer.kt index 89bd828..205d1c6 100644 --- a/backend/src/main/kotlin/me/sujanpoudel/playdeals/jobs/DealSummarizer.kt +++ b/backend/src/main/kotlin/me/sujanpoudel/playdeals/jobs/DealSummarizer.kt @@ -5,8 +5,6 @@ import me.sujanpoudel.playdeals.common.SIMPLE_NAME import me.sujanpoudel.playdeals.common.loggingExecutionTime import me.sujanpoudel.playdeals.domain.entities.formattedCurrentPrice import me.sujanpoudel.playdeals.domain.entities.formattedNormalPrice -import me.sujanpoudel.playdeals.infoNotify -import me.sujanpoudel.playdeals.logger import me.sujanpoudel.playdeals.repositories.DealRepository import me.sujanpoudel.playdeals.repositories.KeyValuesRepository import me.sujanpoudel.playdeals.services.MessagingService @@ -20,15 +18,14 @@ import java.time.OffsetDateTime import java.util.UUID class DealSummarizer( - override val di: DI + override val di: DI, ) : CoJobRequestHandler(), DIAware { - private val dealRepository by instance() private val keyValueRepository by instance() private val messagingService by instance() override suspend fun handleRequest(jobRequest: Request): Unit = loggingExecutionTime( - "$SIMPLE_NAME:: handleRequest" + "$SIMPLE_NAME:: handleRequest", ) { val lastTimestamp = keyValueRepository.get(LAST_SUMMARY_TIMESTAMP)?.let(OffsetDateTime::parse) ?: OffsetDateTime.now() @@ -50,10 +47,10 @@ class DealSummarizer( "$dealsDescription\n\n +${deals.size - maxCount} more..." } else { dealsDescription - } + }, ) } else { - logger.infoNotify("$SIMPLE_NAME:: haven't got any deals since $lastTimestamp") + infoNotify("$SIMPLE_NAME:: haven't got any deals since $lastTimestamp") } keyValueRepository.set(LAST_SUMMARY_TIMESTAMP, OffsetDateTime.now().toString()) @@ -64,6 +61,7 @@ class DealSummarizer( companion object { private val JOB_ID: UUID = UUID.nameUUIDFromBytes("deal-summarizer".toByteArray()) + operator fun invoke(): RecurringJobBuilder = RecurringJobBuilder.aRecurringJob() .withJobRequest(Request()) .withCron(Cron.daily(16)) diff --git a/backend/src/main/kotlin/me/sujanpoudel/playdeals/jobs/ForexFetcher.kt b/backend/src/main/kotlin/me/sujanpoudel/playdeals/jobs/ForexFetcher.kt index 6bc2757..d7c42f8 100644 --- a/backend/src/main/kotlin/me/sujanpoudel/playdeals/jobs/ForexFetcher.kt +++ b/backend/src/main/kotlin/me/sujanpoudel/playdeals/jobs/ForexFetcher.kt @@ -5,7 +5,7 @@ import com.google.gson.reflect.TypeToken import io.vertx.core.json.Json import io.vertx.ext.web.client.WebClient import io.vertx.ext.web.client.WebClientOptions -import io.vertx.kotlin.coroutines.await +import io.vertx.kotlin.coroutines.coAwait import me.sujanpoudel.playdeals.Conf import me.sujanpoudel.playdeals.common.SIMPLE_NAME import me.sujanpoudel.playdeals.common.loggingExecutionTime @@ -26,34 +26,33 @@ import java.util.UUID data class Currency( val name: String, - val symbol: String + val symbol: String, ) -fun loadCurrencies(): HashMap { +fun loadCurrencies(): HashMap { return Gson().fromJson( Thread.currentThread().contextClassLoader.getResource("currencies.json")?.readText() ?: "{}", - object : TypeToken>() {} + object : TypeToken>() {}, ) } class ForexFetcher( override val di: DI, - private val conf: Conf + private val conf: Conf, ) : CoJobRequestHandler(), DIAware { - private val backgroundJobsVerticle by instance() private val webClient by lazy { WebClient.create( backgroundJobsVerticle.vertx, - WebClientOptions().setSsl(false).setDefaultHost("api.exchangeratesapi.io") + WebClientOptions().setSsl(false).setDefaultHost("api.exchangeratesapi.io"), ) } private val repository by instance() override suspend fun handleRequest(jobRequest: Request): Unit = loggingExecutionTime( - "$SIMPLE_NAME:: handleRequest" + "$SIMPLE_NAME:: handleRequest", ) { val rates = getForexRates() logger.info("got ${rates.rates.size} forex rate") @@ -65,7 +64,7 @@ class ForexFetcher( val response = webClient.get("/v1/latest?access_key=${conf.forexApiKey}&format=1&base=EUR") .send() - .await() + .coAwait() .bodyAsString() .let { Json.decodeValue(it) as io.vertx.core.json.JsonObject @@ -78,14 +77,13 @@ class ForexFetcher( timestamp = OffsetDateTime.ofInstant(java.time.Instant.ofEpochSecond(epochSeconds), ZoneOffset.UTC), rates = response.getJsonObject("rates").map { val currency = currencies[it.key] - ConversionRate( currency = it.key, symbol = currency?.symbol ?: "$", name = currency?.name ?: it.key, - rate = (it.value as Number).toFloat() / usdRate + rate = (it.value as Number).toFloat() / usdRate, ) - } + }, ) } @@ -94,6 +92,7 @@ class ForexFetcher( companion object { private val JOB_ID: UUID = UUID.nameUUIDFromBytes("ForexFetch".toByteArray()) + operator fun invoke(): RecurringJobBuilder = RecurringJobBuilder.aRecurringJob() .withJobRequest(Request()) .withName("ForexFetch") diff --git a/backend/src/main/kotlin/me/sujanpoudel/playdeals/jobs/RedditPostsScrapper.kt b/backend/src/main/kotlin/me/sujanpoudel/playdeals/jobs/RedditPostsScrapper.kt index a9fab58..29bebf4 100644 --- a/backend/src/main/kotlin/me/sujanpoudel/playdeals/jobs/RedditPostsScrapper.kt +++ b/backend/src/main/kotlin/me/sujanpoudel/playdeals/jobs/RedditPostsScrapper.kt @@ -4,7 +4,7 @@ import io.vertx.core.json.JsonObject import io.vertx.ext.web.client.WebClient import io.vertx.ext.web.client.WebClientOptions import io.vertx.kotlin.core.json.jsonArrayOf -import io.vertx.kotlin.coroutines.await +import io.vertx.kotlin.coroutines.coAwait import me.sujanpoudel.playdeals.common.SIMPLE_NAME import me.sujanpoudel.playdeals.common.loggingExecutionTime import me.sujanpoudel.playdeals.infoNotify @@ -25,39 +25,40 @@ import java.util.UUID data class RedditPost( val id: String, val content: String, - val createdAt: OffsetDateTime + val createdAt: OffsetDateTime, ) class RedditPostsScrapper( - override val di: DI + override val di: DI, ) : CoJobRequestHandler(), DIAware { - private val verticle by instance() private val keyValueRepository by instance() private val webClient by lazy { WebClient.create( verticle.vertx, - WebClientOptions().setDefaultHost("www.reddit.com") + WebClientOptions().setDefaultHost("www.reddit.com"), ) } private val jobRequestScheduler by instance() override suspend fun handleRequest(jobRequest: Request): Unit = loggingExecutionTime( - "$SIMPLE_NAME:: handleRequest" + "$SIMPLE_NAME:: handleRequest", ) { val lastPostTime = keyValueRepository.get(LAST_REDDIT_POST_TIME)?.let(OffsetDateTime::parse) - val posts = loggingExecutionTime( - "$SIMPLE_NAME:: Fetched reddit post, last created post was at : '$lastPostTime'" - ) { - getLatestRedditPosts(lastPostTime ?: OffsetDateTime.MIN) - } - - val appIds = posts.flatMap { post -> - PLAY_CONSOLE_REGX.findAll(post.content).toList().mapNotNull { - it.groupValues.lastOrNull() + val posts = + loggingExecutionTime( + "$SIMPLE_NAME:: Fetched reddit post, last created post was at : '$lastPostTime'", + ) { + getLatestRedditPosts(lastPostTime ?: OffsetDateTime.MIN) } - }.distinct() + + val appIds = + posts.flatMap { post -> + PLAY_CONSOLE_REGX.findAll(post.content).toList().mapNotNull { + it.groupValues.lastOrNull() + } + }.distinct() logger.infoNotify("$SIMPLE_NAME:: got ${posts.size} new posts (${appIds.size} Links)") @@ -93,13 +94,14 @@ class RedditPostsScrapper( RedditPost( id = data.getString("name"), content = data.getString("selftext").trim().ifBlank { data.getString("url") }, - createdAt = data.getDouble("created").toLong().let { - OffsetDateTime.ofInstant(Instant.ofEpochSecond(it), ZoneOffset.UTC) - } + createdAt = + data.getDouble("created").toLong().let { + OffsetDateTime.ofInstant(Instant.ofEpochSecond(it), ZoneOffset.UTC) + }, ) } } - .await() + .coAwait() .filter { it.createdAt > lastPostTime } @@ -110,6 +112,7 @@ class RedditPostsScrapper( companion object { private val JOB_ID: UUID = UUID.nameUUIDFromBytes("Reddit Posts".toByteArray()) + operator fun invoke(): RecurringJobBuilder = RecurringJobBuilder.aRecurringJob() .withJobRequest(Request()) .withAmountOfRetries(2) diff --git a/backend/src/main/kotlin/me/sujanpoudel/playdeals/repositories/caching/CachingDealRepository.kt b/backend/src/main/kotlin/me/sujanpoudel/playdeals/repositories/caching/CachingDealRepository.kt index 7aa049c..8770ce6 100644 --- a/backend/src/main/kotlin/me/sujanpoudel/playdeals/repositories/caching/CachingDealRepository.kt +++ b/backend/src/main/kotlin/me/sujanpoudel/playdeals/repositories/caching/CachingDealRepository.kt @@ -6,9 +6,8 @@ import me.sujanpoudel.playdeals.logger import me.sujanpoudel.playdeals.repositories.DealRepository class CachingDealRepository( - private val delegate: DealRepository + private val delegate: DealRepository, ) : DealRepository by delegate { - private val cache by lazy { HashMap(0, 0.8f) } diff --git a/backend/src/main/kotlin/me/sujanpoudel/playdeals/repositories/persistent/PersistentDealRepository.kt b/backend/src/main/kotlin/me/sujanpoudel/playdeals/repositories/persistent/PersistentDealRepository.kt index e5b3602..11cbb6e 100644 --- a/backend/src/main/kotlin/me/sujanpoudel/playdeals/repositories/persistent/PersistentDealRepository.kt +++ b/backend/src/main/kotlin/me/sujanpoudel/playdeals/repositories/persistent/PersistentDealRepository.kt @@ -1,6 +1,6 @@ package me.sujanpoudel.playdeals.repositories.persistent -import io.vertx.kotlin.coroutines.await +import io.vertx.kotlin.coroutines.coAwait import io.vertx.sqlclient.SqlClient import me.sujanpoudel.playdeals.common.exec import me.sujanpoudel.playdeals.domain.NewDeal @@ -11,15 +11,15 @@ import me.sujanpoudel.playdeals.repositories.DealRepository import java.time.OffsetDateTime class PersistentDealRepository( - private val sqlClient: SqlClient + private val sqlClient: SqlClient, ) : DealRepository { override suspend fun getAll(skip: Int, take: Int): List { return sqlClient.preparedQuery( """ SELECT * FROM "deal" ORDER BY created_at DESC OFFSET $1 LIMIT $2 - """.trimIndent() + """.trimIndent(), ).exec(skip, take) - .await() + .coAwait() .map { it.asAppDeal() } } @@ -43,9 +43,9 @@ class PersistentDealRepository( type = $13, source = $14 RETURNING * - """.trimIndent() + """.trimIndent(), ).exec(*appDeal.insertValues) - .await() + .coAwait() .first() .asAppDeal() } @@ -54,9 +54,9 @@ class PersistentDealRepository( return sqlClient.preparedQuery( """ DELETE from "deal" where id=$1 RETURNING * - """.trimIndent() + """.trimIndent(), ).exec(id) - .await() + .coAwait() .firstOrNull()?.asAppDeal() } @@ -64,9 +64,9 @@ class PersistentDealRepository( return sqlClient.preparedQuery( """ SELECT * FROM "deal" where offer_expires_in < current_timestamp - """.trimIndent() + """.trimIndent(), ).exec() - .await() + .coAwait() .map { it.asAppDeal() } } @@ -74,9 +74,9 @@ class PersistentDealRepository( return sqlClient.preparedQuery( """ SELECT * FROM "deal" where created_at > $1 - """.trimIndent() + """.trimIndent(), ).exec(since) - .await() + .coAwait() .map { it.asAppDeal() } } @@ -84,9 +84,9 @@ class PersistentDealRepository( return sqlClient.preparedQuery( """ SELECT * FROM "deal" where id = $1 - """.trimIndent() + """.trimIndent(), ).exec(packageName) - .await() + .coAwait() .firstOrNull()?.asAppDeal() } } diff --git a/backend/src/main/kotlin/me/sujanpoudel/playdeals/repositories/persistent/PersistentKeyValuesRepository.kt b/backend/src/main/kotlin/me/sujanpoudel/playdeals/repositories/persistent/PersistentKeyValuesRepository.kt index 2cd7aaa..a605267 100644 --- a/backend/src/main/kotlin/me/sujanpoudel/playdeals/repositories/persistent/PersistentKeyValuesRepository.kt +++ b/backend/src/main/kotlin/me/sujanpoudel/playdeals/repositories/persistent/PersistentKeyValuesRepository.kt @@ -1,6 +1,6 @@ package me.sujanpoudel.playdeals.repositories.persistent -import io.vertx.kotlin.coroutines.await +import io.vertx.kotlin.coroutines.coAwait import io.vertx.sqlclient.SqlClient import me.sujanpoudel.playdeals.common.exec import me.sujanpoudel.playdeals.domain.entities.value @@ -8,18 +8,17 @@ import me.sujanpoudel.playdeals.domain.entities.valueOrNull import me.sujanpoudel.playdeals.repositories.KeyValuesRepository class PersistentKeyValuesRepository( - private val sqlClient: SqlClient + private val sqlClient: SqlClient, ) : KeyValuesRepository { - override suspend fun set(key: String, value: String): String { return sqlClient.preparedQuery( """ INSERT INTO "key_value_store" VALUES ($1,$2) ON CONFLICT(key) DO UPDATE SET value = $2 RETURNING * - """.trimIndent() + """.trimIndent(), ).exec(key, value) - .await() + .coAwait() .first() .value() } @@ -28,9 +27,9 @@ class PersistentKeyValuesRepository( return sqlClient.preparedQuery( """ SELECT * FROM "key_value_store" WHERE key = $1 - """.trimIndent() + """.trimIndent(), ).exec(key) - .await() + .coAwait() .firstOrNull() .valueOrNull() } @@ -38,9 +37,9 @@ class PersistentKeyValuesRepository( override suspend fun delete(key: String) { sqlClient.preparedQuery( """ - DELETE FROM "key_value_store" WHERE key = $1 - """.trimIndent() + DELETE FROM "key_value_store" WHERE key = $1 + """.trimIndent(), ).exec(key) - .await() + .coAwait() } } diff --git a/backend/src/main/kotlin/me/sujanpoudel/playdeals/services/MessagingService.kt b/backend/src/main/kotlin/me/sujanpoudel/playdeals/services/MessagingService.kt index 7d29003..f8fdbf6 100644 --- a/backend/src/main/kotlin/me/sujanpoudel/playdeals/services/MessagingService.kt +++ b/backend/src/main/kotlin/me/sujanpoudel/playdeals/services/MessagingService.kt @@ -28,36 +28,31 @@ suspend fun ApiFuture.awaitIgnoring() { class MessagingService( private val firebaseMessaging: FirebaseMessaging, - private val environment: Environment + private val environment: Environment, ) { - private fun String.asTopic() = if (environment == Environment.PRODUCTION) this else "$this-dev" - suspend fun sendMessageToTopic( - topic: String, - title: String, - body: String, - imageUrl: String? = null - ) { - val message = Message.builder() - .setTopic(topic.asTopic()) - .setNotification( - Notification.builder() - .setTitle(title) - .setBody(body) - .setImage(imageUrl) - .build() - ).setAndroidConfig( - AndroidConfig.builder() - .setCollapseKey(topic) - .setNotification( - AndroidNotification.builder() - .setPriority(AndroidNotification.Priority.HIGH) - .setChannelId(topic) - .build() - ) - .build() - ).build() + suspend fun sendMessageToTopic(topic: String, title: String, body: String, imageUrl: String? = null) { + val message = + Message.builder() + .setTopic(topic.asTopic()) + .setNotification( + Notification.builder() + .setTitle(title) + .setBody(body) + .setImage(imageUrl) + .build(), + ).setAndroidConfig( + AndroidConfig.builder() + .setCollapseKey(topic) + .setNotification( + AndroidNotification.builder() + .setPriority(AndroidNotification.Priority.HIGH) + .setChannelId(topic) + .build(), + ) + .build(), + ).build() firebaseMessaging.sendAsync(message) .awaitIgnoring() @@ -65,18 +60,19 @@ class MessagingService( } suspend inline fun MessagingService.sendMessageForNewDeal(deal: DealEntity) = sendMessageToTopic( - topic = if (deal.currentPrice == 0f) { - Constants.PushNotificationTopic.NEW_FREE_DEAL - } else { - Constants.PushNotificationTopic.NEW_DISCOUNT_DEAL - }, + topic = + if (deal.currentPrice == 0f) { + Constants.PushNotificationTopic.NEW_FREE_DEAL + } else { + Constants.PushNotificationTopic.NEW_DISCOUNT_DEAL + }, title = "New deal found", body = "${deal.name} was ${deal.formattedNormalPrice()} is now ${deal.formattedCurrentPrice()}", - imageUrl = deal.icon + imageUrl = deal.icon, ) suspend inline fun MessagingService.sendMaintenanceLog(message: String) = sendMessageToTopic( topic = Constants.PushNotificationTopic.DEV_LOG, title = "Maintenance Log", - body = message + body = message, ) diff --git a/backend/src/main/kotlin/me/sujanpoudel/playdeals/usecases/DBHealthUseCase.kt b/backend/src/main/kotlin/me/sujanpoudel/playdeals/usecases/DBHealthUseCase.kt index 94c908c..f11aff9 100644 --- a/backend/src/main/kotlin/me/sujanpoudel/playdeals/usecases/DBHealthUseCase.kt +++ b/backend/src/main/kotlin/me/sujanpoudel/playdeals/usecases/DBHealthUseCase.kt @@ -1,20 +1,19 @@ package me.sujanpoudel.playdeals.usecases -import io.vertx.kotlin.coroutines.await +import io.vertx.kotlin.coroutines.coAwait import io.vertx.sqlclient.SqlClient import org.kodein.di.DI import org.kodein.di.instance class DBHealthUseCase( - di: DI + di: DI, ) : UseCase { - private val sqlClient by di.instance() override suspend fun doExecute(input: Unit): Boolean = runCatching { sqlClient.preparedQuery("""SELECT 1""") .execute() - .await() + .coAwait() }.map { rs -> rs.count() == 1 }.getOrDefault(false) diff --git a/backend/src/main/kotlin/me/sujanpoudel/playdeals/usecases/GetDealsUseCase.kt b/backend/src/main/kotlin/me/sujanpoudel/playdeals/usecases/GetDealsUseCase.kt index 8e403fd..c3a7a9e 100644 --- a/backend/src/main/kotlin/me/sujanpoudel/playdeals/usecases/GetDealsUseCase.kt +++ b/backend/src/main/kotlin/me/sujanpoudel/playdeals/usecases/GetDealsUseCase.kt @@ -6,9 +6,8 @@ import org.kodein.di.DI import org.kodein.di.instance class GetDealsUseCase( - di: DI + di: DI, ) : UseCase> { - private val appDealsRepository by di.instance() class Input(val skip: Int, val take: Int) diff --git a/backend/src/main/kotlin/me/sujanpoudel/playdeals/usecases/GetForexUseCase.kt b/backend/src/main/kotlin/me/sujanpoudel/playdeals/usecases/GetForexUseCase.kt index a9ddfbe..dbe407e 100644 --- a/backend/src/main/kotlin/me/sujanpoudel/playdeals/usecases/GetForexUseCase.kt +++ b/backend/src/main/kotlin/me/sujanpoudel/playdeals/usecases/GetForexUseCase.kt @@ -7,7 +7,6 @@ import org.kodein.di.DI import org.kodein.di.instance class GetForexUseCase(di: DI) : UseCase { - private val appDealsRepository by di.instance() override suspend fun doExecute(input: Unit): ForexRate? { diff --git a/backend/src/main/kotlin/me/sujanpoudel/playdeals/usecases/NewDealUseCase.kt b/backend/src/main/kotlin/me/sujanpoudel/playdeals/usecases/NewDealUseCase.kt index 6c36df3..5270e87 100644 --- a/backend/src/main/kotlin/me/sujanpoudel/playdeals/usecases/NewDealUseCase.kt +++ b/backend/src/main/kotlin/me/sujanpoudel/playdeals/usecases/NewDealUseCase.kt @@ -8,9 +8,8 @@ import org.kodein.di.instance import java.util.UUID class NewDealUseCase( - di: DI + di: DI, ) : UseCase { - private val jobRequestScheduler by di.instance() private val dealsRepository by di.instance() diff --git a/backend/src/main/kotlin/me/sujanpoudel/playdeals/usecases/UseCaseEngine.kt b/backend/src/main/kotlin/me/sujanpoudel/playdeals/usecases/UseCaseEngine.kt index eba5669..9f213d8 100644 --- a/backend/src/main/kotlin/me/sujanpoudel/playdeals/usecases/UseCaseEngine.kt +++ b/backend/src/main/kotlin/me/sujanpoudel/playdeals/usecases/UseCaseEngine.kt @@ -19,6 +19,7 @@ interface UseCase { } suspend fun validate(input: Input) {} + suspend fun doExecute(input: Input): Output } @@ -27,9 +28,14 @@ suspend fun RoutingContext.executeUseCase( toContext: suspend () -> Request, toInput: (Request) -> Input, onError: (Throwable) -> Unit = this::handleExceptions, - onSuccess: (Output) -> Unit + onSuccess: (Output) -> Unit, ): Result = runCatching { toContext.invoke() } - .andThen { runCatching { (it as? Validated)?.validate(); it } } + .andThen { + runCatching { + (it as? Validated)?.validate() + it + } + } .andThen { runCatching { toInput.invoke(it) } } .andThen { runCatching { useCase.execute(it) } } .onSuccess(onSuccess) diff --git a/backend/src/test/kotlin/me/sujanpoudel/playdeals/IntegrationTest.kt b/backend/src/test/kotlin/me/sujanpoudel/playdeals/IntegrationTest.kt index adad98c..4e513de 100644 --- a/backend/src/test/kotlin/me/sujanpoudel/playdeals/IntegrationTest.kt +++ b/backend/src/test/kotlin/me/sujanpoudel/playdeals/IntegrationTest.kt @@ -5,7 +5,7 @@ import io.vertx.core.Vertx import io.vertx.ext.web.client.WebClient import io.vertx.ext.web.client.WebClientOptions import io.vertx.junit5.VertxExtension -import io.vertx.kotlin.coroutines.await +import io.vertx.kotlin.coroutines.coAwait import io.vertx.kotlin.coroutines.dispatcher import io.vertx.sqlclient.SqlClient import kotlinx.coroutines.runBlocking @@ -36,29 +36,32 @@ abstract class IntegrationTest(private val vertx: Vertx) { WebClientOptions() .apply { defaultPort = conf.api.port - } + }, ) } - private val conf = Conf( - api = Conf.Api(8888, cors = ".*."), - environment = Environment.TEST, - db = Conf.DB( - host = postgresqlContainer.host, - port = postgresqlContainer.firstMappedPort, - name = DB_NAME, - username = DB_USERNAME, - password = DB_PASSWORD, - 3 - ), - backgroundTask = Conf.BackgroundTask( - false, - "admin", - "admin" - ), - firebaseAuthCredential = "", - forexApiKey = "" - ) + private val conf = + Conf( + api = Conf.Api(8888, cors = ".*."), + environment = Environment.TEST, + db = + Conf.DB( + host = postgresqlContainer.host, + port = postgresqlContainer.firstMappedPort, + name = DB_NAME, + username = DB_USERNAME, + password = DB_PASSWORD, + 3, + ), + backgroundTask = + Conf.BackgroundTask( + false, + "admin", + "admin", + ), + firebaseAuthCredential = "", + forexApiKey = "", + ) var di = configureDI(vertx, conf) @@ -73,7 +76,7 @@ abstract class IntegrationTest(private val vertx: Vertx) { } private fun deployVerticle(): String = runBlocking(vertx.dispatcher()) { - vertx.deployVerticle(di.direct.instance()).await() + vertx.deployVerticle(di.direct.instance()).coAwait() } @BeforeEach @@ -89,33 +92,34 @@ abstract class IntegrationTest(private val vertx: Vertx) { .query(CLEAN_UP_DB_QUERY).execute() .onSuccess { log.info { "Successfully cleaned up dh" } } .onFailure { log.error(it) { "Could not cleanup db" } } - .await() + .coAwait() } @AfterEach fun undeployVerticle() = runBlocking(vertx.dispatcher()) { - vertx.undeploy(deploymentId).await() + vertx.undeploy(deploymentId).coAwait() log.info { "un-deployed deployment id $deploymentId" } } companion object { - @JvmStatic - protected val CLEAN_UP_DB_QUERY = """ + protected val CLEAN_UP_DB_QUERY = + """ DELETE FROM "deal" WHERE True; DELETE FROM "key_value_store" WHERE True; - """.trimIndent() + """.trimIndent() @Container @JvmStatic - val postgresqlContainer: PostgreSQLContainer = PostgreSQLContainer( - DockerImageName.parse(System.getenv("POSTGRES_IMAGE") ?: "postgres:14") - .asCompatibleSubstituteFor("postgres") - ) - .apply { - withDatabaseName(DB_NAME) - withUsername(DB_USERNAME) - withPassword(DB_PASSWORD) - } + val postgresqlContainer: PostgreSQLContainer = + PostgreSQLContainer( + DockerImageName.parse(System.getenv("POSTGRES_IMAGE") ?: "postgres:14") + .asCompatibleSubstituteFor("postgres"), + ) + .apply { + withDatabaseName(DB_NAME) + withUsername(DB_USERNAME) + withPassword(DB_PASSWORD) + } } } diff --git a/backend/src/test/kotlin/me/sujanpoudel/playdeals/MainTest.kt b/backend/src/test/kotlin/me/sujanpoudel/playdeals/MainTest.kt index 0c8e1ee..d8d07dc 100644 --- a/backend/src/test/kotlin/me/sujanpoudel/playdeals/MainTest.kt +++ b/backend/src/test/kotlin/me/sujanpoudel/playdeals/MainTest.kt @@ -12,25 +12,23 @@ import org.junit.jupiter.api.Test class MainTest { @Test fun `Should return a proper conf with all values from env`() { - val env = mutableMapOf( - "ENV" to "DEVELOPMENT", - "APP_PORT" to "123", - "CORS" to "*.example.com", - - "DASHBOARD" to "true", - "DASHBOARD_USER" to "user", - "DASHBOARD_PASS" to "admin", - - "DB_HOST" to "localhost1", - "DB_USERNAME" to "u", - "DB_PASSWORD" to "p", - "DB_POOL_SIZE" to "8", - "DB_PORT" to "3333", - "DB_NAME" to "db-name", - - "FIREBASE_ADMIN_AUTH_CREDENTIALS" to "dGVzdF9jcmVk", - "FOREX_API_KEY" to "forex_key" - ) + val env = + mutableMapOf( + "ENV" to "DEVELOPMENT", + "APP_PORT" to "123", + "CORS" to "*.example.com", + "DASHBOARD" to "true", + "DASHBOARD_USER" to "user", + "DASHBOARD_PASS" to "admin", + "DB_HOST" to "localhost1", + "DB_USERNAME" to "u", + "DB_PASSWORD" to "p", + "DB_POOL_SIZE" to "8", + "DB_PORT" to "3333", + "DB_NAME" to "db-name", + "FIREBASE_ADMIN_AUTH_CREDENTIALS" to "dGVzdF9jcmVk", + "FOREX_API_KEY" to "forex_key", + ) val conf = buildConf(env).unwrap() @@ -55,14 +53,15 @@ class MainTest { @Test fun `Should return a proper conf with some defaults being taken`() { - val env = mutableMapOf( - "DB_PORT" to "3333", - "DB_HOST" to "localhost", - "DB_USERNAME" to "u", - "DB_PASSWORD" to "p", - "FIREBASE_ADMIN_AUTH_CREDENTIALS" to "dGVzdF9jcmVk", - "FOREX_API_KEY" to "forex_key" - ) + val env = + mutableMapOf( + "DB_PORT" to "3333", + "DB_HOST" to "localhost", + "DB_USERNAME" to "u", + "DB_PASSWORD" to "p", + "FIREBASE_ADMIN_AUTH_CREDENTIALS" to "dGVzdF9jcmVk", + "FOREX_API_KEY" to "forex_key", + ) val conf = buildConf(env).unwrap() @@ -87,24 +86,26 @@ class MainTest { @Test fun `Should return all violations`() { - val env = mutableMapOf( - "APP_PORT" to "BAD_APP_PORT", - "ENV" to "prod" - ) + val env = + mutableMapOf( + "APP_PORT" to "BAD_APP_PORT", + "ENV" to "prod", + ) val violations = ((buildConf(env).unwrapError()) as BootstrapException).violations violations.shouldNotBeEmpty() - violations shouldContainExactlyInAnyOrder listOf( - "Invalid 'ENV'", - "Invalid 'APP_PORT'", - "No 'DB_HOST' env var defined!", - "Invalid 'DB_HOST'", - "No 'DB_USERNAME' env var defined!", - "Invalid 'DB_USERNAME'", - "No 'FIREBASE_ADMIN_AUTH_CREDENTIALS' env var defined!", - "Invalid 'FIREBASE_ADMIN_AUTH_CREDENTIALS'", - "No 'FOREX_API_KEY' env var defined!", - "Invalid 'FOREX_API_KEY'" - ) + violations shouldContainExactlyInAnyOrder + listOf( + "Invalid 'ENV'", + "Invalid 'APP_PORT'", + "No 'DB_HOST' env var defined!", + "Invalid 'DB_HOST'", + "No 'DB_USERNAME' env var defined!", + "Invalid 'DB_USERNAME'", + "No 'FIREBASE_ADMIN_AUTH_CREDENTIALS' env var defined!", + "Invalid 'FIREBASE_ADMIN_AUTH_CREDENTIALS'", + "No 'FOREX_API_KEY' env var defined!", + "Invalid 'FOREX_API_KEY'", + ) } } diff --git a/backend/src/test/kotlin/me/sujanpoudel/playdeals/api/deals/GetDealsApiTest.kt b/backend/src/test/kotlin/me/sujanpoudel/playdeals/api/deals/GetDealsApiTest.kt index 73f05dd..68755d3 100644 --- a/backend/src/test/kotlin/me/sujanpoudel/playdeals/api/deals/GetDealsApiTest.kt +++ b/backend/src/test/kotlin/me/sujanpoudel/playdeals/api/deals/GetDealsApiTest.kt @@ -5,7 +5,7 @@ import com.fasterxml.jackson.module.kotlin.readValue import io.kotest.matchers.collections.shouldContainAll import io.kotest.matchers.shouldBe import io.vertx.core.Vertx -import io.vertx.kotlin.coroutines.await +import io.vertx.kotlin.coroutines.coAwait import me.sujanpoudel.playdeals.Constants import me.sujanpoudel.playdeals.IntegrationTest import me.sujanpoudel.playdeals.api.ApiResponse @@ -18,29 +18,31 @@ import me.sujanpoudel.playdeals.repositories.persistent.PersistentDealRepository import org.junit.jupiter.api.Test import java.time.OffsetDateTime -private val newDeal = NewDeal( - id = "id", - name = "name", - icon = "icon", - images = listOf("img0", "img1"), - normalPrice = 12f, - currentPrice = 12f, - currency = "USD", - storeUrl = "store_url", - category = "unknown", - downloads = "12+", - rating = "12", - offerExpiresIn = OffsetDateTime.now(), - type = DealType.ANDROID_APP, - source = Constants.DealSources.APP_DEAL_SUBREDDIT -) +private val newDeal = + NewDeal( + id = "id", + name = "name", + icon = "icon", + images = listOf("img0", "img1"), + normalPrice = 12f, + currentPrice = 12f, + currency = "USD", + storeUrl = "store_url", + category = "unknown", + downloads = "12+", + rating = "12", + offerExpiresIn = OffsetDateTime.now(), + type = DealType.ANDROID_APP, + source = Constants.DealSources.APP_DEAL_SUBREDDIT, + ) class GetDealsApiTest(vertx: Vertx) : IntegrationTest(vertx) { @Test fun `should send error if skip param is less than 0`() = runTest { - val response = httpClient.get("/api/deals/?skip=-1") - .send() - .await() + val response = + httpClient.get("/api/deals/?skip=-1") + .send() + .coAwait() val responseBody = response.bodyAsJsonObject() @@ -50,9 +52,10 @@ class GetDealsApiTest(vertx: Vertx) : IntegrationTest(vertx) { @Test fun `should send error if take param is less than 1`() = runTest { - val response = httpClient.get("/api/deals/?take=0") - .send() - .await() + val response = + httpClient.get("/api/deals/?take=0") + .send() + .coAwait() val responseBody = response.bodyAsJsonObject() @@ -67,9 +70,10 @@ class GetDealsApiTest(vertx: Vertx) : IntegrationTest(vertx) { val app0 = repository.upsert(newDeal) val app1 = repository.upsert(newDeal.copy(id = "id1")) - val response = httpClient.get("/api/deals/") - .send() - .await() + val response = + httpClient.get("/api/deals/") + .send() + .coAwait() val deals: ApiResponse> = di.get().readValue(response.bodyAsString()) @@ -89,7 +93,7 @@ class GetDealsApiTest(vertx: Vertx) : IntegrationTest(vertx) { httpClient.get("/api/deals?skip=1") .send() - .await().also { response -> + .coAwait().also { response -> val deals: ApiResponse> = di.get().readValue(response.bodyAsString()) response.statusCode() shouldBe 200 @@ -98,7 +102,7 @@ class GetDealsApiTest(vertx: Vertx) : IntegrationTest(vertx) { httpClient.get("/api/deals?skip=3") .send() - .await().also { response -> + .coAwait().also { response -> val deals: ApiResponse> = di.get().readValue(response.bodyAsString()) response.statusCode() shouldBe 200 @@ -117,7 +121,7 @@ class GetDealsApiTest(vertx: Vertx) : IntegrationTest(vertx) { httpClient.get("/api/deals?take=2") .send() - .await().also { response -> + .coAwait().also { response -> val deals: ApiResponse> = di.get().readValue(response.bodyAsString()) response.statusCode() shouldBe 200 @@ -126,7 +130,7 @@ class GetDealsApiTest(vertx: Vertx) : IntegrationTest(vertx) { httpClient.get("/api/deals?take=1") .send() - .await().also { response -> + .coAwait().also { response -> val deals: ApiResponse> = di.get().readValue(response.bodyAsString()) response.statusCode() shouldBe 200 diff --git a/backend/src/test/kotlin/me/sujanpoudel/playdeals/api/deals/NewDealApiTest.kt b/backend/src/test/kotlin/me/sujanpoudel/playdeals/api/deals/NewDealApiTest.kt index 860369d..c6890da 100644 --- a/backend/src/test/kotlin/me/sujanpoudel/playdeals/api/deals/NewDealApiTest.kt +++ b/backend/src/test/kotlin/me/sujanpoudel/playdeals/api/deals/NewDealApiTest.kt @@ -3,7 +3,7 @@ package me.sujanpoudel.playdeals.api.deals import io.kotest.matchers.shouldBe import io.vertx.core.Vertx import io.vertx.kotlin.core.json.jsonObjectOf -import io.vertx.kotlin.coroutines.await +import io.vertx.kotlin.coroutines.coAwait import me.sujanpoudel.playdeals.Constants import me.sujanpoudel.playdeals.IntegrationTest import me.sujanpoudel.playdeals.domain.NewDeal @@ -17,12 +17,12 @@ import java.time.OffsetDateTime import java.util.UUID class NewDealApiTest(vertx: Vertx) : IntegrationTest(vertx) { - @Test fun `should send error response if packageName is null`() = runTest { - val response = httpClient.post("/api/deals") - .sendJson(jsonObjectOf()) - .await() + val response = + httpClient.post("/api/deals") + .sendJson(jsonObjectOf()) + .coAwait() val responseBody = response.bodyAsJsonObject() @@ -32,9 +32,10 @@ class NewDealApiTest(vertx: Vertx) : IntegrationTest(vertx) { @Test fun `should send error response if packageName is invalid`() = runTest { - val response = httpClient.post("/api/deals") - .sendJson(jsonObjectOf("packageName" to "11111")) - .await() + val response = + httpClient.post("/api/deals") + .sendJson(jsonObjectOf("packageName" to "11111")) + .coAwait() val responseBody = response.bodyAsJsonObject() @@ -48,9 +49,10 @@ class NewDealApiTest(vertx: Vertx) : IntegrationTest(vertx) { val packageName = "com.example.app" - val response = httpClient.post("/api/deals") - .sendJson(jsonObjectOf("packageName" to packageName)) - .await() + val response = + httpClient.post("/api/deals") + .sendJson(jsonObjectOf("packageName" to packageName)) + .coAwait() val job = storageProvider.getJobById(UUID.nameUUIDFromBytes(packageName.encodeToByteArray())) @@ -66,28 +68,30 @@ class NewDealApiTest(vertx: Vertx) : IntegrationTest(vertx) { val packageName = "com.example.app" - val newDeal = NewDeal( - id = packageName, - name = "name", - icon = "icon", - images = listOf("img0", "img1"), - normalPrice = 12f, - currentPrice = 12f, - currency = "USD", - storeUrl = "store_url", - category = "unknown", - downloads = "12+", - rating = "12", - offerExpiresIn = OffsetDateTime.now(), - type = DealType.ANDROID_APP, - source = Constants.DealSources.APP_DEAL_SUBREDDIT - ) + val newDeal = + NewDeal( + id = packageName, + name = "name", + icon = "icon", + images = listOf("img0", "img1"), + normalPrice = 12f, + currentPrice = 12f, + currency = "USD", + storeUrl = "store_url", + category = "unknown", + downloads = "12+", + rating = "12", + offerExpiresIn = OffsetDateTime.now(), + type = DealType.ANDROID_APP, + source = Constants.DealSources.APP_DEAL_SUBREDDIT, + ) repository.upsert(newDeal) - val response = httpClient.post("/api/deals") - .sendJson(jsonObjectOf("packageName" to packageName)) - .await() + val response = + httpClient.post("/api/deals") + .sendJson(jsonObjectOf("packageName" to packageName)) + .coAwait() response.statusCode() shouldBe 200 } diff --git a/backend/src/test/kotlin/me/sujanpoudel/playdeals/api/forex/GetForexApiTest.kt b/backend/src/test/kotlin/me/sujanpoudel/playdeals/api/forex/GetForexApiTest.kt index 1f1fbc4..43fd0bb 100644 --- a/backend/src/test/kotlin/me/sujanpoudel/playdeals/api/forex/GetForexApiTest.kt +++ b/backend/src/test/kotlin/me/sujanpoudel/playdeals/api/forex/GetForexApiTest.kt @@ -3,7 +3,7 @@ package me.sujanpoudel.playdeals.api.forex import io.kotest.matchers.shouldBe import io.vertx.core.Vertx import io.vertx.core.json.JsonObject -import io.vertx.kotlin.coroutines.await +import io.vertx.kotlin.coroutines.coAwait import me.sujanpoudel.playdeals.IntegrationTest import me.sujanpoudel.playdeals.domain.ConversionRate import me.sujanpoudel.playdeals.domain.ForexRate @@ -16,15 +16,15 @@ import java.time.OffsetDateTime import java.time.ZoneOffset class GetForexApiTest(vertx: Vertx) : IntegrationTest(vertx) { - @Test fun `Key value repo should properly store the forex rate`() = runTest { val repository = di.get() - val forexRate = ForexRate( - timestamp = OffsetDateTime.now().withOffsetSameInstant(ZoneOffset.UTC), - rates = listOf(ConversionRate("USD", "$", "US Dollar", 1.1f)) - ) + val forexRate = + ForexRate( + timestamp = OffsetDateTime.now().withOffsetSameInstant(ZoneOffset.UTC), + rates = listOf(ConversionRate("USD", "$", "US Dollar", 1.1f)), + ) repository.saveForexRate(forexRate) val savedForexRate = repository.getForexRate() @@ -36,17 +36,19 @@ class GetForexApiTest(vertx: Vertx) : IntegrationTest(vertx) { fun `should return forex if there is data`() = runTest { val repository = di.get() - val forexRate = ForexRate( - timestamp = OffsetDateTime.now().withOffsetSameInstant(ZoneOffset.UTC), - rates = listOf(ConversionRate("USD", "$", "US Dollar", 1.1f)) - ) + val forexRate = + ForexRate( + timestamp = OffsetDateTime.now().withOffsetSameInstant(ZoneOffset.UTC), + rates = listOf(ConversionRate("USD", "$", "US Dollar", 1.1f)), + ) repository.saveForexRate(forexRate) - val response = httpClient.get("/api/forex") - .send() - .await() - .bodyAsJsonObject() + val response = + httpClient.get("/api/forex") + .send() + .coAwait() + .bodyAsJsonObject() response.getJsonObject("data").also { data -> OffsetDateTime.parse(data.getString("timestamp")) shouldBe forexRate.timestamp @@ -64,10 +66,11 @@ class GetForexApiTest(vertx: Vertx) : IntegrationTest(vertx) { @Test fun `should return null if there is no data`() = runTest { - val response = httpClient.get("/api/forex") - .send() - .await() - .bodyAsJsonObject() + val response = + httpClient.get("/api/forex") + .send() + .coAwait() + .bodyAsJsonObject() response.getJsonObject("data") shouldBe null } diff --git a/backend/src/test/kotlin/me/sujanpoudel/playdeals/api/health/DBCleanupTest.kt b/backend/src/test/kotlin/me/sujanpoudel/playdeals/api/health/DBCleanupTest.kt index 5d56e23..3d82deb 100644 --- a/backend/src/test/kotlin/me/sujanpoudel/playdeals/api/health/DBCleanupTest.kt +++ b/backend/src/test/kotlin/me/sujanpoudel/playdeals/api/health/DBCleanupTest.kt @@ -2,14 +2,13 @@ package me.sujanpoudel.playdeals.api.health import io.kotest.matchers.shouldBe import io.vertx.core.Vertx -import io.vertx.kotlin.coroutines.await +import io.vertx.kotlin.coroutines.coAwait import io.vertx.sqlclient.SqlClient import me.sujanpoudel.playdeals.IntegrationTest import me.sujanpoudel.playdeals.get import org.junit.jupiter.api.Test class DBCleanupTest(vertx: Vertx) : IntegrationTest(vertx) { - @Test fun `Does cleanup`() = runTest { val sqlClient = di.get() @@ -18,9 +17,10 @@ class DBCleanupTest(vertx: Vertx) : IntegrationTest(vertx) { .query(CLEAN_UP_DB_QUERY).execute() .onFailure { it.printStackTrace() } - val totalDeals = sqlClient.preparedQuery("""select count(*) from deal """) - .execute() - .await().first().getInteger(0) + val totalDeals = + sqlClient.preparedQuery("""select count(*) from deal """) + .execute() + .coAwait().first().getInteger(0) totalDeals shouldBe 0 } diff --git a/backend/src/test/kotlin/me/sujanpoudel/playdeals/api/health/HealthTest.kt b/backend/src/test/kotlin/me/sujanpoudel/playdeals/api/health/HealthTest.kt index 6ec411d..c0b4dd5 100644 --- a/backend/src/test/kotlin/me/sujanpoudel/playdeals/api/health/HealthTest.kt +++ b/backend/src/test/kotlin/me/sujanpoudel/playdeals/api/health/HealthTest.kt @@ -2,15 +2,14 @@ package me.sujanpoudel.playdeals.api.health import io.kotest.matchers.shouldBe import io.vertx.core.Vertx -import io.vertx.kotlin.coroutines.await +import io.vertx.kotlin.coroutines.coAwait import me.sujanpoudel.playdeals.IntegrationTest import org.junit.jupiter.api.Test class HealthTest(vertx: Vertx) : IntegrationTest(vertx) { - @Test fun `GET liveness should return 200`() = runTest { - val response = httpClient.get(8888, "localhost", "/health/liveness").send().await() + val response = httpClient.get(8888, "localhost", "/health/liveness").send().coAwait() response.statusCode() shouldBe 200 val responseJson = response.bodyAsJsonObject() @@ -19,7 +18,7 @@ class HealthTest(vertx: Vertx) : IntegrationTest(vertx) { @Test fun `GET readiness should return 200`() = runTest { - val response = httpClient.get(8888, "localhost", "/health/readiness").send().await() + val response = httpClient.get(8888, "localhost", "/health/readiness").send().coAwait() val responseJson = response.bodyAsJsonObject() response.statusCode() shouldBe 200 diff --git a/backend/src/test/kotlin/me/sujanpoudel/playdeals/repositories/CachingDealRepositoryTest.kt b/backend/src/test/kotlin/me/sujanpoudel/playdeals/repositories/CachingDealRepositoryTest.kt index 8977660..312c1dc 100644 --- a/backend/src/test/kotlin/me/sujanpoudel/playdeals/repositories/CachingDealRepositoryTest.kt +++ b/backend/src/test/kotlin/me/sujanpoudel/playdeals/repositories/CachingDealRepositoryTest.kt @@ -17,7 +17,6 @@ import org.junit.jupiter.api.BeforeEach import org.junit.jupiter.api.Test class CachingDealRepositoryTest(vertx: Vertx) : IntegrationTest(vertx) { - private lateinit var persistentDealRepository: PersistentDealRepository private lateinit var repository: CachingDealRepository diff --git a/backend/src/test/kotlin/me/sujanpoudel/playdeals/repositories/PersistentDealRepositoryTest.kt b/backend/src/test/kotlin/me/sujanpoudel/playdeals/repositories/PersistentDealRepositoryTest.kt index 255d385..69a81d2 100644 --- a/backend/src/test/kotlin/me/sujanpoudel/playdeals/repositories/PersistentDealRepositoryTest.kt +++ b/backend/src/test/kotlin/me/sujanpoudel/playdeals/repositories/PersistentDealRepositoryTest.kt @@ -5,7 +5,7 @@ import io.kotest.matchers.collections.shouldContainInOrder import io.kotest.matchers.equality.shouldBeEqualToComparingFields import io.kotest.matchers.shouldBe import io.vertx.core.Vertx -import io.vertx.kotlin.coroutines.await +import io.vertx.kotlin.coroutines.coAwait import io.vertx.sqlclient.SqlClient import me.sujanpoudel.playdeals.Constants import me.sujanpoudel.playdeals.IntegrationTest @@ -19,36 +19,37 @@ import org.junit.jupiter.api.Test import java.time.OffsetDateTime class PersistentDealRepositoryTest(vertx: Vertx) : IntegrationTest(vertx) { - private val repository by lazy { di.get() } private val sqlClient by lazy { di.get() } - private val newDeal = NewDeal( - id = "id", - name = "name", - icon = "icon", - images = listOf("img0", "img1"), - normalPrice = 12f, - currentPrice = 12f, - currency = "USD", - storeUrl = "store_url", - category = "unknown", - downloads = "12+", - rating = "12", - offerExpiresIn = OffsetDateTime.now(), - type = DealType.ANDROID_APP, - source = Constants.DealSources.APP_DEAL_SUBREDDIT - ) + private val newDeal = + NewDeal( + id = "id", + name = "name", + icon = "icon", + images = listOf("img0", "img1"), + normalPrice = 12f, + currentPrice = 12f, + currency = "USD", + storeUrl = "store_url", + category = "unknown", + downloads = "12+", + rating = "12", + offerExpiresIn = OffsetDateTime.now(), + type = DealType.ANDROID_APP, + source = Constants.DealSources.APP_DEAL_SUBREDDIT, + ) @Test fun `should create new app deal in db`() = runTest { val appDeal = repository.upsert(newDeal) - val appDealFromDb = sqlClient.preparedQuery(""" SELECT * from "deal" where id=$1""") - .exec(newDeal.id) - .await() - .first() - .asAppDeal() + val appDealFromDb = + sqlClient.preparedQuery(""" SELECT * from "deal" where id=$1""") + .exec(newDeal.id) + .coAwait() + .first() + .asAppDeal() appDeal.shouldBeEqualToComparingFields(appDealFromDb) } @@ -59,11 +60,12 @@ class PersistentDealRepositoryTest(vertx: Vertx) : IntegrationTest(vertx) { repository.upsert(newDeal.copy(name = "Updated Name")) - val appDealFromDb = sqlClient.preparedQuery(""" SELECT * from "deal" where id=$1""") - .exec(newDeal.id) - .await() - .first() - .asAppDeal() + val appDealFromDb = + sqlClient.preparedQuery(""" SELECT * from "deal" where id=$1""") + .exec(newDeal.id) + .coAwait() + .first() + .asAppDeal() appDealFromDb.name.shouldBe("Updated Name") } @@ -75,7 +77,7 @@ class PersistentDealRepositoryTest(vertx: Vertx) : IntegrationTest(vertx) { sqlClient.preparedQuery("""SELECT * from "deal" where id=$1""") .exec(newDeal.id) - .await() + .coAwait() .rowCount() shouldBe 0 } diff --git a/backend/src/test/kotlin/me/sujanpoudel/playdeals/repositories/PersistentKeyValueRepositoryTest.kt b/backend/src/test/kotlin/me/sujanpoudel/playdeals/repositories/PersistentKeyValueRepositoryTest.kt index 74f1802..fd488b0 100644 --- a/backend/src/test/kotlin/me/sujanpoudel/playdeals/repositories/PersistentKeyValueRepositoryTest.kt +++ b/backend/src/test/kotlin/me/sujanpoudel/playdeals/repositories/PersistentKeyValueRepositoryTest.kt @@ -2,7 +2,7 @@ package me.sujanpoudel.playdeals.repositories import io.kotest.matchers.shouldBe import io.vertx.core.Vertx -import io.vertx.kotlin.coroutines.await +import io.vertx.kotlin.coroutines.coAwait import io.vertx.sqlclient.SqlClient import me.sujanpoudel.playdeals.IntegrationTest import me.sujanpoudel.playdeals.common.exec @@ -12,7 +12,6 @@ import org.junit.jupiter.api.Test import java.time.OffsetDateTime class PersistentKeyValueRepositoryTest(vertx: Vertx) : IntegrationTest(vertx) { - private val repository by lazy { di.get() } private val sqlClient by lazy { di.get() } @@ -20,11 +19,12 @@ class PersistentKeyValueRepositoryTest(vertx: Vertx) : IntegrationTest(vertx) { fun `should create new entry on db`() = runTest { val value = repository.set(KEY, "test") - val valueFromDb = sqlClient.preparedQuery(""" SELECT * from "key_value_store" where key=$1""") - .exec(KEY) - .await() - .first() - .getString("value") + val valueFromDb = + sqlClient.preparedQuery(""" SELECT * from "key_value_store" where key=$1""") + .exec(KEY) + .coAwait() + .first() + .getString("value") value shouldBe valueFromDb } @@ -35,11 +35,12 @@ class PersistentKeyValueRepositoryTest(vertx: Vertx) : IntegrationTest(vertx) { val updated = repository.set(KEY, "test1") - val fromDb = sqlClient.preparedQuery(""" SELECT * from "key_value_store" where key=$1""") - .exec(KEY) - .await() - .first() - .value() + val fromDb = + sqlClient.preparedQuery(""" SELECT * from "key_value_store" where key=$1""") + .exec(KEY) + .coAwait() + .first() + .value() fromDb shouldBe updated } diff --git a/build.gradle.kts b/build.gradle.kts index be67f8c..f951076 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -1,23 +1,18 @@ @file:Suppress("UnstableApiUsage") import org.gradle.api.tasks.testing.logging.TestLogEvent -import org.jetbrains.kotlin.gradle.plugin.KotlinPlatformJvmPlugin import org.jetbrains.kotlin.gradle.tasks.KotlinCompile +import org.jlleitschuh.gradle.ktlint.KtlintExtension import org.jlleitschuh.gradle.ktlint.KtlintPlugin plugins { - kotlin("jvm") version Versions.KOTLIN - id("org.jlleitschuh.gradle.ktlint") version "11.5.0" -} - -buildscript { - repositories { - mavenCentral() - } + alias(libs.plugins.kotlinJvm) + alias(libs.plugins.shadow) apply false + alias(libs.plugins.jib) apply false + alias(libs.plugins.ktlint) } allprojects { - apply() apply() apply() @@ -25,20 +20,26 @@ allprojects { mavenCentral() } - val compileKotlin by tasks.getting(KotlinCompile::class) { - kotlinOptions { - jvmTarget = "17" - } + task("preCommitHook") { + dependsOn(tasks.ktlintCheck) } - val compileTestKotlin by tasks.getting(KotlinCompile::class) { - kotlinOptions { - jvmTarget = "17" + extensions.configure { + version = rootProject.libs.versions.ktlint.get() + enableExperimentalRules = false + coloredOutput = true + + filter { + exclude { + it.file.absoluteFile.startsWith(layout.buildDirectory.asFile.get().absolutePath) + } } } - task("preCommitHook") { - dependsOn(tasks.ktlintCheck) + tasks.withType { + compilerOptions { + allWarningsAsErrors.set(true) + } } } @@ -48,7 +49,7 @@ tasks.withType { events = setOf( TestLogEvent.PASSED, TestLogEvent.SKIPPED, - TestLogEvent.FAILED + TestLogEvent.FAILED, ) } } @@ -62,6 +63,6 @@ task("installPreCommitHook") { } } -tasks.withType() { +tasks.withType { dependsOn("installPreCommitHook") } diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml new file mode 100644 index 0000000..69683bd --- /dev/null +++ b/gradle/libs.versions.toml @@ -0,0 +1,88 @@ +[versions] + +## artifacts +android-compileSdk = "34" +android-minSdk = "29" +android-targetSdk = "34" + +## plugins +kotlin = "2.0.0" +shadow = "7.1.2" +jib = "3.4.3" +klint-plugin = "12.0.3" + +## Libraries +kotlinx-coroutines-core = "1.8.1" +vertx = "4.5.8" +jackson = "2.17.1" +flyway = "10.14.0" +postgresql = "42.7.3" +ongress-scram = "2.1" +firebase-admin = "9.3.0" + +kotlin-result = "2.0.0" +kodein = "7.22.0" +slfj4 = "2.0.13" +jvm-logger = "3.0.5" + +job-runner = "7.2.0" + +ktlint = "1.2.1" + +## testing +junit-jupiter = "5.10.2" +kotest = "5.9.1" +test-container = "1.19.8" +mockk = "1.13.11" + +[libraries] +kotlinx-coroutines-core = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-core", version.ref = "kotlinx-coroutines-core" } +kotlinx-stdlib = { module = "org.jetbrains.kotlin:kotlin-stdlib", version.ref = "kotlin" } + +vertx-depchain = { module = "io.vertx:vertx-stack-depchain", version.ref = "vertx" } +vertx-core = { module = "io.vertx:vertx-core" } +vertx-web = { module = "io.vertx:vertx-web" } +vertx-pgClient = { module = "io.vertx:vertx-pg-client" } +vertx-coroutines = { module = "io.vertx:vertx-lang-kotlin-coroutines" } +vertx-kotlin = { module = "io.vertx:vertx-lang-kotlin" } +vertx-healthCheck = { module = "io.vertx:vertx-health-check" } +vertx-webClient = { module = "io.vertx:vertx-web-client" } +vertx-junit5 = { module = "io.vertx:vertx-junit5" } + +kotlinResult = { module = "com.michael-bull.kotlin-result:kotlin-result", version.ref = "kotlin-result" } + +flyway-core = { module = "org.flywaydb:flyway-core", version.ref = "flyway" } +flyway-postgresql = { module = "org.flywaydb:flyway-database-postgresql", version.ref = "flyway" } +postgresql = { module = "org.postgresql:postgresql", version.ref = "postgresql" } +scramOngressClient = { module = "com.ongres.scram:client", version.ref = "ongress-scram" } + +slf4j-api = { module = "org.slf4j:slf4j-api", version.ref = "slfj4" } +slf4j-simpe = { module = "org.slf4j:slf4j-simple", version.ref = "slfj4" } +kotlinLoggingJvm = { module = "io.github.microutils:kotlin-logging-jvm", version.ref = "jvm-logger" } + +jackson-databind = { module = "com.fasterxml.jackson.core:jackson-databind", version.ref = "jackson" } +jackson-moduleKotlin = { module = "com.fasterxml.jackson.module:jackson-module-kotlin", version.ref = "jackson" } +jackson-datatype-jsr310 = { module = "com.fasterxml.jackson.datatype:jackson-datatype-jsr310", version.ref = "jackson" } + +kodein = { module = "org.kodein.di:kodein-di", version.ref = "kodein" } + +jobrunr = { module = "org.jobrunr:jobrunr", version.ref = "job-runner" } +jobrunr-kotlin = { module = "org.jobrunr:jobrunr-kotlin-1.8-support", version.ref = "job-runner" } + +firebaseAdmin = { module = "com.google.firebase:firebase-admin", version.ref = "firebase-admin" } + +## Testing +junit-jupiter = { module = "org.junit.jupiter:junit-jupiter", version.ref = "junit-jupiter" } +kotest-assertions-core = { module = "io.kotest:kotest-assertions-core", version.ref = "kotest" } +mockk = { module = "io.mockk:mockk", version.ref = "mockk" } + +testcontainers = { module = "org.testcontainers:testcontainers", version.ref = "test-container" } +testcontainers-junit = { module = "org.testcontainers:junit-jupiter", version.ref = "test-container" } +testcontainers-postgresql = { module = "org.testcontainers:postgresql", version.ref = "test-container" } + +[plugins] +kotlinJvm = { id = "org.jetbrains.kotlin.jvm", version.ref = "kotlin" } +ktlint = { id = "org.jlleitschuh.gradle.ktlint", version.ref = "klint-plugin" } +shadow = { id = "com.github.johnrengelman.shadow", version.ref = "shadow" } +jib = { id = "com.google.cloud.tools.jib", version.ref = "jib" } + diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index 84a0b92..48c0a02 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -1,5 +1,5 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-8.2.1-bin.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-8.7-bin.zip zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists diff --git a/settings.gradle.kts b/settings.gradle.kts index c9cc5b5..27da8f1 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -1,3 +1,11 @@ rootProject.name = "deals" +dependencyResolutionManagement { + repositories { + google() + gradlePluginPortal() + mavenCentral() + } +} + include("backend")