diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index c9cac7a48116769bcc6169e1fa1f22ee35281cd0..2ab223c051e303877ce167ad9d09596f159b977f 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -8,6 +8,7 @@ ksp = "2.0.21-1.0.28" ktor = "3.0.1" kotlinx-serialization = "1.7.3" cache4k = "0.13.0" +statelyConcurrentCollections = "2.1.0" slf4j = "2.0.16" @@ -19,6 +20,7 @@ arrow = "1.2.4" # Main Koin version - for core, android deps koin = "4.0.0" koinTest = "4.0.0" +gradleBuildConfigPlugin = "5.5.1" # Koin version for Compose multiplatform koinComposeMultiplatform = "4.0.0" mosaic = "0.13.0" @@ -42,6 +44,7 @@ jline = "3.27.1" appdirs = "1.2.0" kstore = "0.9.1" kmpIo = "0.1.5" +kotlinSemver = "2.0.0" kotlinx-dataframe = "0.13.1" klaxon = "5.6" @@ -74,6 +77,7 @@ ktor-client-java = { group = "io.ktor", name = "ktor-client-java", version.ref = ktor-client-android = { group = "io.ktor", name = "ktor-client-android", version.ref = "ktor" } ktor-test = { group = "io.ktor", name = "ktor-client-mock", version.ref = "ktor" } cache4k = { group = "io.github.reactivecircus.cache4k", name = "cache4k", version.ref = "cache4k" } +statelyConcurrentCollections = { group = "co.touchlab", name = "stately-concurrent-collections", version.ref = "statelyConcurrentCollections" } slf4j = { group = "org.slf4j", name = "slf4j-simple", version.ref = "slf4j" } kotlinx-serialization-json = { module = "org.jetbrains.kotlinx:kotlinx-serialization-json", version.ref = "kotlinx-serialization" } @@ -93,6 +97,7 @@ appdirs = { module = "ca.gosyer:kotlin-multiplatform-appdirs", version.ref = "ap kstore = { module = "io.github.xxfast:kstore", version.ref = "kstore" } kstore-file = { module = "io.github.xxfast:kstore-file", version.ref = "kstore" } kmpIo = { module = "io.github.skolson:kmp-io", version.ref = "kmpIo" } +kotlinSemver = { module = "io.github.z4kn4fein:semver", version.ref = "kotlinSemver" } sandwich = { module = "com.github.skydoves:sandwich", version.ref = "sandwich" } sandwich-ktor = { module = "com.github.skydoves:sandwich-ktor", version.ref = "sandwich" } @@ -183,6 +188,7 @@ ksp = { id = "com.google.devtools.ksp", version.ref = "ksp" } ktorfit = { id = "de.jensklingenberg.ktorfit", version.ref = "ktorfit" } kotlinxSerialization = { id = "org.jetbrains.kotlin.plugin.serialization", version.ref = "kotlin" } kotestMultiplatform = { id = "io.kotest.multiplatform", version.ref = "kotest" } +buildConfig = { id = "com.github.gmazzo.buildconfig", version.ref = "gradleBuildConfigPlugin" } dokka = { id = "org.jetbrains.dokka", version.ref = "dokka" } spotless = { id = "com.diffplug.spotless", version.ref = "spotless" } gitSemVer = { id = "org.danilopianini.git-sensitive-semantic-versioning", version.ref = "gitSemVer" } diff --git a/temerity/build.gradle.kts b/temerity/build.gradle.kts index f98fbe7efe856748e5d37d83b0c0852cdaa019eb..4c08df2c332ec0323f42134d95020f8ede82371c 100644 --- a/temerity/build.gradle.kts +++ b/temerity/build.gradle.kts @@ -26,6 +26,7 @@ plugins { alias(libs.plugins.ktorfit) alias(libs.plugins.kotlinxSerialization) alias(libs.plugins.kotestMultiplatform) + alias(libs.plugins.buildConfig) id("convention.formattingLib") id("convention.version") @@ -43,6 +44,10 @@ buildscript { } } +buildConfig { + buildConfigField("LIB_VERSION", provider { "${project.version}" }) +} + gitSemVer { commitNameBasedUpdateStrategy(ConventionalCommit::semanticVersionUpdate) } @@ -80,7 +85,8 @@ kotlin { implementation(libs.ktorfit.lib) implementation(libs.kotlinx.coroutines.core) implementation(libs.kermit) - implementation(libs.cache4k) + // TODO: Re-add cache4k when updated for Kotlin 2.0.21 + implementation(libs.statelyConcurrentCollections) implementation(libs.kmpIo) } } @@ -118,6 +124,8 @@ kotlin { implementation(libs.koin.test) implementation(libs.koin.test.junit5) + + implementation(libs.kotlinSemver) } } } diff --git a/temerity/src/commonMain/kotlin/edu/ucsc/its/temerity/TemClientConfig.kt b/temerity/src/commonMain/kotlin/edu/ucsc/its/temerity/TemClientConfig.kt index a7372e974a21539852a64daba54998d237b4f674..cf6d4610dc67f584889f91479dc9f9c1383d33a9 100644 --- a/temerity/src/commonMain/kotlin/edu/ucsc/its/temerity/TemClientConfig.kt +++ b/temerity/src/commonMain/kotlin/edu/ucsc/its/temerity/TemClientConfig.kt @@ -37,6 +37,7 @@ import kotlin.time.Duration * @property useWebTimeout Specifies whether to use a timeout other than default for web requests * @property webTimeout Specifies the timeout [Duration] to use when making web requests * @property cacheTimeout Specifies the timeout [Duration] to use when storing cache entries. Affects keep-alive time for cached instance data like user role type fields. + * @property threadCount Specifies the number of threads to use for each client instance. Defaults to 2 at a minimum */ @TemDsl public class TemClientConfig { @@ -54,6 +55,8 @@ public class TemClientConfig { // Default cache timeout is set to 15 minutes if this is left unspecified; @see [edu.ucsc.its.temerity.core.Temerity] init block public var cacheTimeout: Duration? = null + public var threadCount: Int? = null + /** * Configures the HttpClient with the provided block. * @param () -> HttpClient The block to configure the HttpClient. diff --git a/temerity/src/commonMain/kotlin/edu/ucsc/its/temerity/TemerityApi.kt b/temerity/src/commonMain/kotlin/edu/ucsc/its/temerity/TemerityApi.kt index c0ab3d5dec0fcc35641200802be56d3ff6e6409a..692c59d386ceb8af23cc77cff7d4d917786de4d1 100644 --- a/temerity/src/commonMain/kotlin/edu/ucsc/its/temerity/TemerityApi.kt +++ b/temerity/src/commonMain/kotlin/edu/ucsc/its/temerity/TemerityApi.kt @@ -225,10 +225,16 @@ public interface TemerityApi { public suspend fun refreshCachedUserRoles(): List<String> - public suspend fun getCachedUserRoles(refresh: Boolean): List<String> + /** + * Fetches the user roles from the platform cached locally on the client. + * @param refresh Whether to fetch the roles if not present locally. + */ + public suspend fun getCachedUserRoles(refresh: Boolean = true): List<String> + + public fun version(): String } -// Objects used by Temerity implementations, and possibly library consumers +// Objects used by library consumers and re-implementations of the Temerity API spec public val AUDIT_LOG_REQUEST_DATE_FORMAT: DateTimeFormat<LocalDate> = LocalDate.Format { dayOfMonth() diff --git a/temerity/src/commonMain/kotlin/edu/ucsc/its/temerity/Util.kt b/temerity/src/commonMain/kotlin/edu/ucsc/its/temerity/Util.kt index 593167c9a00b87287c2f2ab4aef324e778bbbd5b..d14b8880bac8848e840bd382a0f549224fa55a93 100644 --- a/temerity/src/commonMain/kotlin/edu/ucsc/its/temerity/Util.kt +++ b/temerity/src/commonMain/kotlin/edu/ucsc/its/temerity/Util.kt @@ -17,7 +17,7 @@ */ package edu.ucsc.its.temerity -import edu.ucsc.its.temerity.core.TemerityLibrary.applicationScope +import edu.ucsc.its.temerity.core.Temerity import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Job import kotlinx.coroutines.SupervisorJob @@ -37,8 +37,7 @@ internal fun currentDate(): LocalDate = Clock.System.todayIn(currentTz()) internal fun currentTime(): LocalTime = thisInstant().toLocalDateTime(currentTz()).time -internal fun createJobScope(coroutineContext: CoroutineContext = applicationScope, allowIndependentFailure: Boolean = false): CoroutineScope = if (allowIndependentFailure) { - CoroutineScope(SupervisorJob() + coroutineContext) -} else { - CoroutineScope(Job() + coroutineContext) +internal fun Temerity.createJobScope(coroutineContext: CoroutineContext = clientCoroutineScope, allowIndependentFailure: Boolean = false): CoroutineScope { + val parentJob = if (allowIndependentFailure) SupervisorJob() else Job() + return CoroutineScope(coroutineContext + parentJob) } diff --git a/temerity/src/commonMain/kotlin/edu/ucsc/its/temerity/core/DispatcherFactory.kt b/temerity/src/commonMain/kotlin/edu/ucsc/its/temerity/core/DispatcherFactory.kt new file mode 100644 index 0000000000000000000000000000000000000000..1d775c60a7364db210b3a3844d0ae7eb4d5c4926 --- /dev/null +++ b/temerity/src/commonMain/kotlin/edu/ucsc/its/temerity/core/DispatcherFactory.kt @@ -0,0 +1,47 @@ +/* + * Designed and developed in 2022-2024 by William Walker (wnwalker@ucsc.edu) + * Copyright 2022-2024 The Regents of the University of California. All rights reserved. + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; version 2.1 of the License. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + */ +package edu.ucsc.its.temerity.core + +import edu.ucsc.its.temerity.core.Temerity.Companion.DEFAULT_MINIMUM_THREAD_COUNT +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.SupervisorJob +import kotlin.coroutines.CoroutineContext + +internal object DispatcherFactory { + + internal fun createDispatcher(threadCount: Int): CoroutineContext = SupervisorJob() + Dispatchers.IO.limitedParallelism(threadCount) + + private fun availableThreads() = Runtime.getRuntime().availableProcessors().minus(1) + + internal fun calculateMaxThreads(defaultMinimumThreadCount: Int) = availableThreads().coerceAtLeast(defaultMinimumThreadCount) + + internal fun setThreadCount(maximumThreadCount: Int? = null): Int = when (maximumThreadCount) { + null -> calculateMaxThreads(DEFAULT_MINIMUM_THREAD_COUNT) + else -> { + if (maximumThreadCount > availableThreads()) { + // TODO: Log warning that the configured thread count is higher than the number of available threads + calculateMaxThreads(DEFAULT_MINIMUM_THREAD_COUNT) + } + if (maximumThreadCount < DEFAULT_MINIMUM_THREAD_COUNT) { + // TODO: Log warning that the configured thread count is lower than the default minimum thread pool size + calculateMaxThreads(DEFAULT_MINIMUM_THREAD_COUNT) + } + maxOf(maximumThreadCount, calculateMaxThreads(DEFAULT_MINIMUM_THREAD_COUNT)) + } + } +} diff --git a/temerity/src/commonMain/kotlin/edu/ucsc/its/temerity/core/HttpClientFactory.kt b/temerity/src/commonMain/kotlin/edu/ucsc/its/temerity/core/HttpClientFactory.kt index 3adbd44ebfb7fd1108319b03781c16ee998e0722..920110d43bd9c8b441ae4f7cd877502089e27413 100644 --- a/temerity/src/commonMain/kotlin/edu/ucsc/its/temerity/core/HttpClientFactory.kt +++ b/temerity/src/commonMain/kotlin/edu/ucsc/its/temerity/core/HttpClientFactory.kt @@ -18,7 +18,7 @@ package edu.ucsc.its.temerity.core import edu.ucsc.its.temerity.TemClientConfig -import edu.ucsc.its.temerity.core.TemerityLibrary.koin +import edu.ucsc.its.temerity.core.Temerity.Companion.DEFAULT_WEB_TIMEOUT import io.ktor.client.HttpClient import io.ktor.client.HttpClientConfig import io.ktor.client.engine.HttpClientEngine @@ -28,6 +28,7 @@ import io.ktor.client.plugins.logging.LogLevel import io.ktor.client.plugins.logging.Logging import io.ktor.client.request.header import io.ktor.http.URLProtocol +import org.koin.core.context.GlobalContext.get import kotlin.time.DurationUnit import co.touchlab.kermit.Logger as KermitLogger import io.ktor.client.plugins.logging.Logger as KtorLogger @@ -43,7 +44,6 @@ internal object HttpClientFactory { * It configures the client to log all requests and responses if enableNetworkLogs is true. TODO: read this as a build setting * It also sets a default request header with the provided authToken. * @param httpClientEngine The HttpClientEngine to use for the HttpClient. - * @param timeoutDuration The duration in milliseconds to wait for a response before timing out. * @param config A block specifying the following options: * - Whether to log all requests and responses. * - Service URL to make requests to. @@ -69,7 +69,7 @@ internal object HttpClientFactory { if (config.optDebugEnabled) { if (config.httpClientLoggingBlock == null) { install(Logging) { - val kermit = koin.get<KermitLogger>() + val kermit = get().get<KermitLogger>() logger = object : KtorLogger { override fun log(message: String) { kermit.d(message) @@ -90,7 +90,7 @@ internal object HttpClientFactory { expectSuccess = config.expectSuccess if (config.useWebTimeout) { - val defaultWebTimeout = TemerityLibrary.DEFAULT_WEB_TIMEOUT.toLong(DurationUnit.MILLISECONDS) + val defaultWebTimeout = DEFAULT_WEB_TIMEOUT.toLong(DurationUnit.MILLISECONDS) val configuredWebTimeout = config.webTimeout?.toLong(DurationUnit.MILLISECONDS) install(HttpTimeout) { connectTimeoutMillis = configuredWebTimeout ?: defaultWebTimeout diff --git a/temerity/src/commonMain/kotlin/edu/ucsc/its/temerity/core/Library.kt b/temerity/src/commonMain/kotlin/edu/ucsc/its/temerity/core/Library.kt deleted file mode 100644 index 57b9f8b6e6bd547712c318da778c44804e1f253e..0000000000000000000000000000000000000000 --- a/temerity/src/commonMain/kotlin/edu/ucsc/its/temerity/core/Library.kt +++ /dev/null @@ -1,59 +0,0 @@ -/* - * Designed and developed in 2022-2024 by William Walker (wnwalker@ucsc.edu) - * Copyright 2022-2024 The Regents of the University of California. All rights reserved. - * - * This library is free software; you can redistribute it and/or - * modify it under the terms of the GNU Lesser General Public - * License as published by the Free Software Foundation; version 2.1 of the License. - * - * This library is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU - * Lesser General Public License for more details. - * - * You should have received a copy of the GNU Lesser General Public - * License along with this library; if not, write to the Free Software - * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA - */ -package edu.ucsc.its.temerity.core - -import edu.ucsc.its.temerity.TemClientConfig -import edu.ucsc.its.temerity.di.libModule -import edu.ucsc.its.temerity.di.loggerModule -import edu.ucsc.its.temerity.platformModule -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.SupervisorJob -import org.koin.core.Koin -import org.koin.dsl.koinApplication -import kotlin.time.Duration -import kotlin.time.Duration.Companion.minutes - -/** - * TemerityLibrary is the main entry point for the Temerity library. - * - * This object initializes the Koin dependency injection framework and returns a library or builder instance - * - */ -public object TemerityLibrary { - public val DEFAULT_WEB_TIMEOUT: Duration = 2.minutes - internal val applicationScope = SupervisorJob() + Dispatchers.IO.limitedParallelism(2) - - private val koinApp = koinApplication { - modules( - loggerModule(), - platformModule(), - libModule, - ) - } - internal val koin: Koin = koinApp.koin -} - -/** - * Create a Temerity instance using Kotlin-DSL. - * Adapted from the tmdb-api project: https://github.com/MoviebaseApp/tmdb-kotlin/raw/refs/heads/main/tmdb-api/src/commonMain/kotlin/app/moviebase/tmdb/Tmdb4.kt - */ -@TemDsl -public fun Temerity(block: TemClientConfig.() -> Unit): Temerity { - val config = TemClientConfig().apply(block) - return Temerity(config) -} diff --git a/temerity/src/commonMain/kotlin/edu/ucsc/its/temerity/core/LoggerFactory.kt b/temerity/src/commonMain/kotlin/edu/ucsc/its/temerity/core/LoggerFactory.kt new file mode 100644 index 0000000000000000000000000000000000000000..ef8b67b094f090c42f5bcab14435443912d32a6e --- /dev/null +++ b/temerity/src/commonMain/kotlin/edu/ucsc/its/temerity/core/LoggerFactory.kt @@ -0,0 +1,35 @@ +/* + * Designed and developed in 2022-2024 by William Walker (wnwalker@ucsc.edu) + * Copyright 2022-2024 The Regents of the University of California. All rights reserved. + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; version 2.1 of the License. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + */ +package edu.ucsc.its.temerity.core + +import co.touchlab.kermit.Logger +import co.touchlab.kermit.NoTagFormatter +import co.touchlab.kermit.Severity +import co.touchlab.kermit.loggerConfigInit +import co.touchlab.kermit.platformLogWriter + +internal object LoggerFactory { + + internal fun createLogger(tag: String?): Logger = Logger( + config = loggerConfigInit( + platformLogWriter(NoTagFormatter), + minSeverity = Severity.Debug, + ), + tag = tag ?: "TemerityLib", + ) +} diff --git a/temerity/src/commonMain/kotlin/edu/ucsc/its/temerity/core/Temerity.kt b/temerity/src/commonMain/kotlin/edu/ucsc/its/temerity/core/Temerity.kt index 896c0ad21a9ee2768b1214459305d7c4a57de4d2..60fec02979202e2fccfc13dceb168b893e7a11c5 100644 --- a/temerity/src/commonMain/kotlin/edu/ucsc/its/temerity/core/Temerity.kt +++ b/temerity/src/commonMain/kotlin/edu/ucsc/its/temerity/core/Temerity.kt @@ -17,6 +17,7 @@ */ package edu.ucsc.its.temerity.core +import co.touchlab.stately.collections.ConcurrentMutableMap import com.skydoves.sandwich.ApiResponse import com.skydoves.sandwich.StatusCode import com.skydoves.sandwich.getOrThrow @@ -24,12 +25,14 @@ import com.skydoves.sandwich.ktor.executeApiResponse import com.skydoves.sandwich.ktor.statusCode import com.skydoves.sandwich.onSuccess import edu.ucsc.its.temerity.AuditLogSortOrder +import edu.ucsc.its.temerity.BuildConfig import edu.ucsc.its.temerity.TemClientConfig import edu.ucsc.its.temerity.TemerityApi import edu.ucsc.its.temerity.api.PlatformApi import edu.ucsc.its.temerity.core.JsonFactory.buildJson -import edu.ucsc.its.temerity.core.TemerityLibrary.koin import edu.ucsc.its.temerity.createJobScope +import edu.ucsc.its.temerity.di.libModule +import edu.ucsc.its.temerity.di.loggerModule import edu.ucsc.its.temerity.extensions.applyAuditLogFormat import edu.ucsc.its.temerity.extensions.applyScheduledSessionDateFormat import edu.ucsc.its.temerity.model.AuditLogEntry @@ -44,8 +47,8 @@ import edu.ucsc.its.temerity.model.User import edu.ucsc.its.temerity.model.UserGroup import edu.ucsc.its.temerity.model.UserRecordingSession import edu.ucsc.its.temerity.model.UserUpdate +import edu.ucsc.its.temerity.platformModule import edu.ucsc.its.temerity.sortByCreationDate -import io.github.reactivecircus.cache4k.Cache import io.ktor.client.statement.HttpResponse import io.ktor.util.cio.toByteArray import io.ktor.utils.io.ByteReadChannel @@ -59,8 +62,13 @@ import kotlinx.datetime.minus import kotlinx.serialization.SerializationException import kotlinx.serialization.encodeToString import kotlinx.serialization.json.Json +import org.koin.core.Koin +import org.koin.core.KoinApplication import org.koin.core.context.GlobalContext.get import org.koin.core.parameter.parametersOf +import org.koin.dsl.koinApplication +import kotlin.coroutines.CoroutineContext +import kotlin.time.Duration import kotlin.time.Duration.Companion.minutes import co.touchlab.kermit.Logger as KermitLogger @@ -70,15 +78,29 @@ import co.touchlab.kermit.Logger as KermitLogger public class Temerity internal constructor( private val config: TemClientConfig, ) : TemerityApi { - /** * Constructor for Temerity */ internal constructor(temApiToken: String) : this(TemClientConfig.withToken(temApiToken)) + public companion object { + public val DEFAULT_WEB_TIMEOUT: Duration = 2.minutes + + // TODO: Use this as cache4k expiration time + public val DEFAULT_CACHE_EXPIRATION: Duration = 15.minutes + internal const val DEFAULT_MINIMUM_THREAD_COUNT: Int = 2 + } + + override fun version(): String = BuildConfig.LIB_VERSION + private val json: Json = buildJson() - private val platformApi: PlatformApi - private val cachedUserRoleList: Cache<Int, String> + + @Suppress("MemberVisibilityCanBePrivate") + internal val temerityKoinApp: KoinApplication + internal val koin: Koin + private var platformApi: PlatformApi + private var cachedUserRoleList: ConcurrentMutableMap<Int, String> + internal var clientCoroutineScope: CoroutineContext init { check(!config.serviceToken.isNullOrBlank()) { @@ -89,13 +111,26 @@ public class Temerity internal constructor( "Service url must be provided. Set it using TemClientConfig.serviceUrl(url)" } + temerityKoinApp = koinApplication { + modules( + loggerModule(), + platformModule(), + libModule, + ) + } + koin = temerityKoinApp.koin + clientCoroutineScope = koin.get<CoroutineContext> { + when (val optThreadCount = config.threadCount) { + null -> parametersOf(DispatcherFactory.calculateMaxThreads(DEFAULT_MINIMUM_THREAD_COUNT)) + else -> parametersOf(DispatcherFactory.setThreadCount(optThreadCount)) + } + } + platformApi = koin.get<PlatformApi> { parametersOf(config) } - cachedUserRoleList = Cache.Builder<Int, String>() - .expireAfterWrite(config.cacheTimeout ?: 15.minutes) - .build() + cachedUserRoleList = ConcurrentMutableMap() } /** @@ -164,8 +199,9 @@ public class Temerity internal constructor( val returnedUsersResponse = platformApi.getUsers().executeApiResponse<String>().getOrThrow() val returnedUserList = json.decodeFromString<List<User>>(returnedUsersResponse) val roleTypes = returnedUserList.map { it.userType }.distinct() + cachedUserRoleList.clear() roleTypes.forEach { - cachedUserRoleList.put(it.hashCode(), it) + cachedUserRoleList[it.hashCode()] = it } return roleTypes } @@ -174,7 +210,7 @@ public class Temerity internal constructor( if (refresh) { return refreshCachedUserRoles() } - val returnedUserRoles = cachedUserRoleList.asMap().values.toList() + val returnedUserRoles = cachedUserRoleList.values.toList() return returnedUserRoles.ifEmpty { refreshCachedUserRoles() } @@ -403,3 +439,19 @@ public class Temerity internal constructor( public override suspend fun getStorageAnalyticsReport(groupId: Long): ByteArray = platformApi.getStorageAnalyticsReport(groupId).executeApiResponse<ByteReadChannel>().getOrThrow().toByteArray(limit = 2000000000) // Limit file downloads to 2 GB } + +/** + * Create a Temerity instance using Kotlin-DSL. + * Adapted from the tmdb-api project: https://github.com/MoviebaseApp/tmdb-kotlin/raw/refs/heads/main/tmdb-api/src/commonMain/kotlin/app/moviebase/tmdb/Tmdb4.kt + */ +@TemDsl +public fun Temerity(block: TemClientConfig.() -> Unit): Temerity { + val config = TemClientConfig().apply(block) + return Temerity(config) +} + +/** + * Create a Temerity Client Configuration instance using Kotlin-DSL. + */ +@TemDsl +public fun TemClientConfig(block: TemClientConfig.() -> Unit): TemClientConfig = TemClientConfig().apply(block) diff --git a/temerity/src/commonMain/kotlin/edu/ucsc/its/temerity/di/Koin.kt b/temerity/src/commonMain/kotlin/edu/ucsc/its/temerity/di/Koin.kt index 92400c898f7ac651fe51ceb7556436fece7e64e2..8d053379682b8c0d2f03eba0dc3b3c9ca5cf595c 100644 --- a/temerity/src/commonMain/kotlin/edu/ucsc/its/temerity/di/Koin.kt +++ b/temerity/src/commonMain/kotlin/edu/ucsc/its/temerity/di/Koin.kt @@ -17,23 +17,17 @@ */ package edu.ucsc.its.temerity.di -import co.touchlab.kermit.NoTagFormatter -import co.touchlab.kermit.Severity -import co.touchlab.kermit.loggerConfigInit -import co.touchlab.kermit.platformLogWriter import com.skydoves.sandwich.ktorfit.ApiResponseConverterFactory import de.jensklingenberg.ktorfit.ktorfit import edu.ucsc.its.temerity.TemClientConfig import edu.ucsc.its.temerity.api.PlatformApi import edu.ucsc.its.temerity.api.createPlatformApi +import edu.ucsc.its.temerity.core.DispatcherFactory.createDispatcher import edu.ucsc.its.temerity.core.HttpClientFactory.buildHttpClient -import edu.ucsc.its.temerity.core.TemerityLibrary.koin +import edu.ucsc.its.temerity.core.LoggerFactory.createLogger import io.ktor.client.HttpClient -import org.koin.core.Koin -import org.koin.core.component.KoinComponent import org.koin.core.parameter.parametersOf import org.koin.dsl.module -import co.touchlab.kermit.Logger as KermitLogger internal fun loggerModule() = module { factory<co.touchlab.kermit.Logger> { tag -> @@ -61,16 +55,7 @@ internal val libModule = module { } ktorfit.createPlatformApi() } -} - -private fun createLogger(tag: String?): KermitLogger = KermitLogger( - config = loggerConfigInit( - platformLogWriter(NoTagFormatter), - minSeverity = Severity.Debug, - ), - tag = tag ?: "TemerityLib", -) - -internal abstract class TemerityKoinComponent : KoinComponent { - override fun getKoin(): Koin = koin + single { (threadCount: Int) -> + createDispatcher(threadCount) + } } diff --git a/temerity/src/jvmTest/kotlin/edu/ucsc/its/temerity/test/DevUtilityTests.kt b/temerity/src/jvmTest/kotlin/edu/ucsc/its/temerity/test/DevUtilityTests.kt index 565875c2a00f42a90a583552d0ab15828d8fb796..df9b3ff7a6937b24947f031df55ea9abd0f28fae 100644 --- a/temerity/src/jvmTest/kotlin/edu/ucsc/its/temerity/test/DevUtilityTests.kt +++ b/temerity/src/jvmTest/kotlin/edu/ucsc/its/temerity/test/DevUtilityTests.kt @@ -22,9 +22,6 @@ import edu.ucsc.its.temerity.core.Temerity import edu.ucsc.its.temerity.di.loggerModule import io.kotest.core.extensions.Extension import io.kotest.core.spec.style.FunSpec -import kotlinx.datetime.Clock -import kotlinx.datetime.TimeZone -import kotlinx.datetime.todayIn import org.dotenv.vault.dotenvVault import org.koin.core.parameter.parametersOf import org.koin.test.KoinTest @@ -44,7 +41,6 @@ class DevUtilityTests : val dotenv = dotenvVault() lateinit var testTemerity: Temerity - val today = Clock.System.todayIn(TimeZone.currentSystemDefault()) beforeTest { testTemerity = Temerity { diff --git a/temerity/src/jvmTest/kotlin/edu/ucsc/its/temerity/test/ProdUtilityTests.kt b/temerity/src/jvmTest/kotlin/edu/ucsc/its/temerity/test/ProdUtilityTests.kt index 0f6deec359cb1b16f0d1208c285fdb22af50d7b6..098fb938d396ebffb480cc5b031e4ee369ba3b53 100644 --- a/temerity/src/jvmTest/kotlin/edu/ucsc/its/temerity/test/ProdUtilityTests.kt +++ b/temerity/src/jvmTest/kotlin/edu/ucsc/its/temerity/test/ProdUtilityTests.kt @@ -22,9 +22,6 @@ import edu.ucsc.its.temerity.core.Temerity import edu.ucsc.its.temerity.di.loggerModule import io.kotest.core.extensions.Extension import io.kotest.core.spec.style.FunSpec -import kotlinx.datetime.Clock -import kotlinx.datetime.TimeZone -import kotlinx.datetime.todayIn import org.dotenv.vault.dotenvVault import org.koin.core.parameter.parametersOf import org.koin.test.KoinTest @@ -44,7 +41,6 @@ class ProdUtilityTests : val dotenv = dotenvVault() lateinit var testTemerity: Temerity - val today = Clock.System.todayIn(TimeZone.currentSystemDefault()) beforeTest { testTemerity = Temerity { diff --git a/temerity/src/jvmTest/kotlin/edu/ucsc/its/temerity/test/TemerityDevTest.kt b/temerity/src/jvmTest/kotlin/edu/ucsc/its/temerity/test/TemerityDevTest.kt index 3ba2506dab26aff2d4818a16c9a7971953896a6e..86610e3637007d1b16091aaa1a256278ef39e36b 100644 --- a/temerity/src/jvmTest/kotlin/edu/ucsc/its/temerity/test/TemerityDevTest.kt +++ b/temerity/src/jvmTest/kotlin/edu/ucsc/its/temerity/test/TemerityDevTest.kt @@ -23,8 +23,12 @@ import com.skydoves.sandwich.ktor.getStatusCode import edu.ucsc.its.temerity.AuditLogSortOrder.NEW_FIRST import edu.ucsc.its.temerity.core.Temerity import edu.ucsc.its.temerity.currentDate +import edu.ucsc.its.temerity.di.loggerModule import edu.ucsc.its.temerity.model.EventType.NEW_LOG_IN import edu.ucsc.its.temerity.model.NewUser +import io.github.z4kn4fein.semver.Version +import io.github.z4kn4fein.semver.toVersion +import io.kotest.core.extensions.Extension import io.kotest.core.spec.style.FunSpec import io.kotest.matchers.file.shouldNotBeEmpty import io.kotest.matchers.shouldBe @@ -34,19 +38,26 @@ import kotlinx.datetime.DateTimeUnit import kotlinx.datetime.TimeZone import kotlinx.datetime.minus import kotlinx.datetime.plus -import kotlinx.datetime.toLocalDateTime import kotlinx.datetime.todayIn import org.dotenv.vault.dotenvVault import org.jetbrains.kotlinx.dataframe.api.toDataFrame import org.jetbrains.kotlinx.dataframe.io.writeCSV +import org.koin.core.component.inject +import org.koin.core.parameter.parametersOf +import org.koin.test.KoinTest import java.io.File import kotlin.uuid.ExperimentalUuidApi import kotlin.uuid.Uuid @OptIn(ExperimentalUuidApi::class) class TemerityDevTest : - FunSpec({ + FunSpec(), + KoinTest { + override fun extensions(): List<Extension> = listOf(koinExtension(loggerModule())) + private val kermit: Logger by inject { parametersOf("TemerityDevTest") } + + init { val dotenv = dotenvVault() lateinit var testTemerity: Temerity @@ -57,9 +68,17 @@ class TemerityDevTest : } } + test("Temerity client returns a correctly-formatted version String") { + val returnedVersion = testTemerity.version().toVersion() + kermit.d { "Returned client version: $returnedVersion" } + assert(testTemerity.version().isNotEmpty()) + assert(returnedVersion < Version(0, 1, 0)) + assert(returnedVersion.isPreRelease) + } + test("Get all audit log entries of type \"New Login\" from the past month") { runBlocking { - val currentDate = Clock.System.todayIn(TimeZone.currentSystemDefault()) + val currentDate = currentDate() val pastDate = currentDate.minus(1, DateTimeUnit.MONTH) val returnedAuditLogEntries = testTemerity.getAuditLogEntries( @@ -133,13 +152,6 @@ class TemerityDevTest : } } - test("test datetime tostring formatting") { - runBlocking { - val time = Clock.System.now().toLocalDateTime(TimeZone.currentSystemDefault()) - println("Current time: $time") - } - } - test("Get all audit log entries of type \"New Login\" from the past day") { runBlocking { val testEventType = NEW_LOG_IN @@ -188,4 +200,5 @@ class TemerityDevTest : Logger.d(response.toString()) } } - }) + } +}