diff --git a/build.gradle.kts b/build.gradle.kts index 2948802..0df1e57 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -43,20 +43,28 @@ kotlin { val commonMain by getting { dependencies { + val ktorVersion = "2.3.+" implementation(kotlin("stdlib-common")) implementation("org.jetbrains.kotlinx:kotlinx-datetime:0.+") implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.6.+") implementation("org.jetbrains.kotlinx:kotlinx-serialization-json:1.5.+") - implementation("io.ktor:ktor-client-core:2.3.+") - implementation("io.ktor:ktor-client-auth:2.3.+") - implementation("io.ktor:ktor-client-logging:2.3.+") - implementation("io.ktor:ktor-client-serialization:2.3.+") - implementation("io.ktor:ktor-client-content-negotiation:2.3.+") - implementation("io.ktor:ktor-serialization-kotlinx-json:2.3.+") implementation("com.squareup.okio:okio:3.4.+") implementation("net.mamoe.yamlkt:yamlkt:0.+") implementation("com.apollographql.apollo3:apollo-api:4.+") implementation("com.apollographql.apollo3:apollo-runtime:4.+") + implementation("com.github.ajalt.clikt:clikt:4.2.+") + implementation("io.ktor:ktor-client-core:$ktorVersion") + implementation("io.ktor:ktor-client-auth:$ktorVersion") + implementation("io.ktor:ktor-client-logging:$ktorVersion") + implementation("io.ktor:ktor-client-serialization:$ktorVersion") + implementation("io.ktor:ktor-client-content-negotiation:$ktorVersion") + implementation("io.ktor:ktor-serialization-kotlinx-json:$ktorVersion") + implementation("io.ktor:ktor-server-core:$ktorVersion") + implementation("io.ktor:ktor-server-host-common:$ktorVersion") + implementation("io.ktor:ktor-server-cio:$ktorVersion") + implementation("io.ktor:ktor-server-cors:$ktorVersion") + implementation("io.ktor:ktor-server-forwarded-header:$ktorVersion") + implementation("io.ktor:ktor-server-content-negotiation:$ktorVersion") } } @@ -65,6 +73,7 @@ kotlin { implementation(kotlin("test")) implementation("com.squareup.okio:okio-fakefilesystem:3.4.+") implementation("com.willowtreeapps.assertk:assertk:0.+") + implementation("io.ktor:ktor-server-test-host:2.3.+") } } diff --git a/gradle.properties b/gradle.properties index e0646f0..6489cc8 100644 --- a/gradle.properties +++ b/gradle.properties @@ -9,6 +9,6 @@ org.gradle.jvmargs=-Xmx4096m # Project properties config.group = xyz.marinkovic.milos config.artifact = codestats -config.version = 0.5.0 +config.version = 0.6.0 config.gitHubRepoOwner = milosmns config.gitHubRepoName = code-stats diff --git a/src/commonMain/kotlin/Main.kt b/src/commonMain/kotlin/Main.kt index 8dc04a1..2c2f9bc 100644 --- a/src/commonMain/kotlin/Main.kt +++ b/src/commonMain/kotlin/Main.kt @@ -1,102 +1,26 @@ -import calculator.di.provideGenericLongMetricCalculators +import commands.CommandLineArguments +import commands.CommandLineArguments.Mode.FETCH +import commands.CommandLineArguments.Mode.PRINT +import commands.CommandLineArguments.Mode.PURGE +import commands.CommandLineArguments.Mode.REPORT +import commands.CommandLineArguments.Mode.SERVE +import commands.di.provideFetchCommand +import commands.di.providePrintCommand +import commands.di.providePurgeCommand +import commands.di.provideReportCommand +import commands.di.provideServeCommand import components.data.TeamHistoryConfig -import history.TeamHistory -import history.filter.transform.RepositoryDateTransform -import history.github.di.provideGitHubHistory -import history.github.di.provideGitHubHistoryConfig -import history.storage.di.provideStoredHistory -import kotlinx.coroutines.runBlocking -import kotlinx.datetime.LocalDate import utils.fromFile -fun main(): Unit = runBlocking { - /************************************************************************* - THESE ARE TEMPORARY EXPERIMENTS, NOT PART OF THE FINAL PRODUCT - *************************************************************************/ - - println("\n== Code Stats CLI ==\n") - - print("Loading configuration... ") - val teamHistoryConfig = TeamHistoryConfig.fromFile("src/commonMain/resources/sample.config.yaml") - println("Done.") - - println(teamHistoryConfig.simpleFormat) - - val history: TeamHistory = provideGitHubHistory( - teamHistoryConfig = teamHistoryConfig, - gitHubHistoryConfig = provideGitHubHistoryConfig(), - ) - - try { - // NETWORK EXPERIMENTS -// println("Loading team history...") -// val fetched = mutableListOf() -// teamHistoryConfig.teams.forEach { team -> -// println("Loading for team ${team.title}...") -// team.discussionRepositories.forEach { repoName -> -// println("Loading discussion repository $repoName...") -// fetched += history.fetchRepository(repoName, includeCodeReviews = false, includeDiscussions = true) -// } -// team.codeRepositories.forEach { repoName -> -// println("Loading code repository $repoName...") -// fetched += history.fetchRepository(repoName, includeCodeReviews = true, includeDiscussions = false) -// } -// } - - // STORAGE EXPERIMENTS - val storage = provideStoredHistory(teamHistoryConfig) -// storage.purgeAll() -// val storedUnsorted = mutableListOf() -// fetched.forEach { -// storage.storeRepositoryDeep(it) -// storedUnsorted += storage.fetchRepository( -// it.name, -// includeCodeReviews = true, -// includeDiscussions = true, -// ) -// } - - val stored = storage.fetchAllRepositories().map { - storage.fetchRepository( - it.name, - includeCodeReviews = true, - includeDiscussions = true, - ) - } - - stored.forEach { repo -> - println(repo.simpleFormat) - repo.codeReviews.forEach { - println("\t r#${it.number} ${it.createdAt} >> ${it.mergedAt} >> ${it.closedAt}") - } - repo.discussions.forEach { - println("\t d#${it.number} ${it.createdAt} >> ${it.closedAt}") - } - println("-- ${repo.fullName} --\n") - } - - println("Filter by date? DD.MM.YYYY (empty for no filter)") - val dateString = readln().trim() - val filtered = if (dateString.isNotEmpty()) { - val day = dateString.substringBefore(".").toInt() - val month = dateString.substringAfter(".").substringBefore(".").toInt() - val year = dateString.substringAfterLast(".").toInt() - val date = LocalDate(year, month, day) - val transform = RepositoryDateTransform(date, date) - stored.map(transform) - } else stored - - // OTHER EXPERIMENTS - provideGenericLongMetricCalculators().forEach { - val metric = it.calculate(filtered) - println(metric.simpleFormat) - println("-- ${metric.name} --\n") - } - } catch (e: Throwable) { - println("CRITICAL FAILURE! \n\n * ${e.message} * \n\n") - e.printStackTrace() - } finally { - history.close() - } - +fun main(args: Array) { + val arguments = CommandLineArguments().load(args) + val teamHistoryConfig = TeamHistoryConfig.fromFile(arguments.configFile) + + when (arguments.mode) { + SERVE -> provideServeCommand(teamHistoryConfig) + FETCH -> provideFetchCommand(teamHistoryConfig) + REPORT -> provideReportCommand(teamHistoryConfig) + PRINT -> providePrintCommand(teamHistoryConfig) + PURGE -> providePurgeCommand(teamHistoryConfig) + }.run() } diff --git a/src/commonMain/kotlin/commands/CommandLineArguments.kt b/src/commonMain/kotlin/commands/CommandLineArguments.kt new file mode 100644 index 0000000..35f7335 --- /dev/null +++ b/src/commonMain/kotlin/commands/CommandLineArguments.kt @@ -0,0 +1,38 @@ +package commands + +import com.github.ajalt.clikt.core.CliktCommand +import com.github.ajalt.clikt.parameters.options.default +import com.github.ajalt.clikt.parameters.options.help +import com.github.ajalt.clikt.parameters.options.option +import com.github.ajalt.clikt.parameters.types.enum +import commands.CommandLineArguments.Mode.REPORT + +class CommandLineArguments : CliktCommand( + name = "codestats", + help = "Code Stats Utility. Run with --help for more.", +) { + + enum class Mode { SERVE, FETCH, REPORT, PRINT, PURGE } + + val mode: Mode by option() + .enum(ignoreCase = true) { it.name.lowercase() } + .default(REPORT) + .help( + listOf( + "[Serve] launches a server and prints the access URL", + "[Fetch] fetches fresh git data and overwrites the stored history", + "[Report] a short report on what data is available", + "[Print] calculates and prints the code stats to stdout", + "[Purge] deletes all previously stored data", + ).joinToString(". ") + ) + + val configFile: String by option() + .default("src/commonMain/resources/sample.config.yaml") + .help("Path to the configuration YAML file") + + override fun run() = Unit + + fun load(args: Array) = apply { main(args) } + +} diff --git a/src/commonMain/kotlin/commands/cli/FetchCommand.kt b/src/commonMain/kotlin/commands/cli/FetchCommand.kt new file mode 100644 index 0000000..04bf62b --- /dev/null +++ b/src/commonMain/kotlin/commands/cli/FetchCommand.kt @@ -0,0 +1,68 @@ +package commands.cli + +import components.data.Repository +import components.data.TeamHistoryConfig +import history.TeamHistory +import history.storage.StoredHistory +import history.storage.config.StorageConfig +import kotlinx.coroutines.Runnable +import kotlinx.coroutines.runBlocking + +class FetchCommand( + private val teamHistoryConfig: TeamHistoryConfig, + private val teamHistory: TeamHistory, + private val storageConfig: StorageConfig, + private val storedHistory: StoredHistory, +) : Runnable { + + override fun run() = runBlocking { + println("== File configuration ==") + println(teamHistoryConfig.simpleFormat) + + println("\n== History fetch ==") + val fetched = mutableListOf() + try { + print("This will start a new data fetch and may impact rate limiting. Continue? (y/n) ") + if (readln().lowercase().trim() != "y") { + println("Aborted. No requests were executed.") + return@runBlocking + } + teamHistoryConfig.teams.forEach { team -> + println("Looking into team ${team.title}...") + team.discussionRepositories.forEach { + println("Fetching discussion history for $it...") + fetched += teamHistory.fetchRepository( + repository = it, + includeCodeReviews = false, + includeDiscussions = true, + ) + } + team.codeRepositories.forEach { + println("Fetching code/review history for $it...") + fetched += teamHistory.fetchRepository( + repository = it, + includeCodeReviews = true, + includeDiscussions = false, + ) + } + } + println("Done. Fetched ${fetched.size} repositories.") + } catch (t: Throwable) { + println("Failed to complete the task. Error: ${t.message}") + return@runBlocking + } finally { + teamHistory.close() + } + + println("\n== History storage ==") + print("Storing fetched data will overwrite existing data. Continue? (y/n) ") + if (readln().lowercase().trim() != "y") { + println("Aborted. If rate limiter is applied to your git remote, wait before trying again.") + return@runBlocking + } + println("Now storing ${fetched.size} repositories locally...") + fetched.forEach(storedHistory::storeRepositoryDeep) + println("\nDone. Your fetched data is at ${storageConfig.databasePath}") + } + +} diff --git a/src/commonMain/kotlin/commands/cli/PrintCommand.kt b/src/commonMain/kotlin/commands/cli/PrintCommand.kt new file mode 100644 index 0000000..a23d3a5 --- /dev/null +++ b/src/commonMain/kotlin/commands/cli/PrintCommand.kt @@ -0,0 +1,40 @@ +package commands.cli + +import calculator.di.provideGenericLongMetricCalculators +import components.data.TeamHistoryConfig +import history.filter.transform.RepositoryDateTransform +import history.storage.StoredHistory +import kotlinx.coroutines.Runnable + +class PrintCommand( + private val teamHistoryConfig: TeamHistoryConfig, + private val storedHistory: StoredHistory, +) : Runnable { + + override fun run() { + println("== File configuration ==") + println(teamHistoryConfig.simpleFormat) + + println("\n== Metrics ==") + val storedRepos = storedHistory.fetchAllRepositories().map { + storedHistory.fetchRepository( + name = it.name, + includeCodeReviews = true, + includeDiscussions = true, + ) + } + val transform = RepositoryDateTransform(teamHistoryConfig.startDate, teamHistoryConfig.endDate) + val filteredRepos = storedRepos.map(transform) + .filter { repo -> repo.codeReviews.isNotEmpty() || repo.discussions.isNotEmpty() } + print("Print details with date filtering applied? (y/n) ") + val dataSet = if (readln().lowercase().trim() == "y") filteredRepos else storedRepos + println("OK.\n") + + provideGenericLongMetricCalculators().forEach { calculator -> + val metric = calculator.calculate(dataSet) + println(metric.simpleFormat) + println("-- ${metric.name} --\n") + } + } + +} diff --git a/src/commonMain/kotlin/commands/cli/PurgeCommand.kt b/src/commonMain/kotlin/commands/cli/PurgeCommand.kt new file mode 100644 index 0000000..704c0fa --- /dev/null +++ b/src/commonMain/kotlin/commands/cli/PurgeCommand.kt @@ -0,0 +1,28 @@ +package commands.cli + +import components.data.TeamHistoryConfig +import history.storage.StoredHistory +import kotlinx.coroutines.Runnable + +class PurgeCommand( + private val teamHistoryConfig: TeamHistoryConfig, + private val storedHistory: StoredHistory, +) : Runnable { + + override fun run() { + println("== File configuration ==") + println(teamHistoryConfig.simpleFormat) + + println("\n== Purge ==") + println("Purging will permanently delete all locally stored data.") + print("There is no data recovery. Continue? (y/n) ") + if (readln().lowercase().trim() != "y") { + println("Aborted. No data was deleted.") + return + } + println("Now purging all locally stored data...") + storedHistory.purgeAll() + println("Done.") + } + +} diff --git a/src/commonMain/kotlin/commands/cli/ReportCommand.kt b/src/commonMain/kotlin/commands/cli/ReportCommand.kt new file mode 100644 index 0000000..59f98f0 --- /dev/null +++ b/src/commonMain/kotlin/commands/cli/ReportCommand.kt @@ -0,0 +1,56 @@ +package commands.cli + +import components.data.TeamHistoryConfig +import history.filter.transform.RepositoryDateTransform +import history.storage.StoredHistory +import kotlinx.coroutines.Runnable + +class ReportCommand( + private val teamHistoryConfig: TeamHistoryConfig, + private val storedHistory: StoredHistory, +) : Runnable { + + override fun run() { + println("== File configuration ==") + println(teamHistoryConfig.simpleFormat) + + println("\n== Local storage ==") + val storedReposShallow = storedHistory.fetchAllRepositories() + println("Total repositories stored locally: ${storedReposShallow.size}") + if (storedReposShallow.isNotEmpty()) { + println(storedReposShallow.joinToString("\n") { " · ${it.fullName}" }) + } + + println("\n== Data scope ==") + val range = "[${teamHistoryConfig.startDate} > ${teamHistoryConfig.endDate}]" + val storedReposDeep = storedReposShallow.map { + storedHistory.fetchRepository( + name = it.name, + includeCodeReviews = true, + includeDiscussions = true, + ) + } + val transform = RepositoryDateTransform(teamHistoryConfig.startDate, teamHistoryConfig.endDate) + val filteredRepos = storedReposDeep.map(transform) + .filter { repo -> repo.codeReviews.isNotEmpty() || repo.discussions.isNotEmpty() } + if (filteredRepos.isNotEmpty()) { + println("Data found for requested dates $range.") + } else { + println("No data found for requested dates $range.") + } + + println("\n== Details ==") + print("Print details per repository? (y/n) ") + if (readln().lowercase().trim() == "y") { + print("Print details with date filtering applied? (y/n) ") + val dataSet = if (readln().lowercase().trim() == "y") filteredRepos else storedReposDeep + println("OK.\n") + if (dataSet.isEmpty()) { + println("No data found for the given criteria.") + } else { + println(dataSet.joinToString("\n") { it.simpleFormat }) + } + } + } + +} diff --git a/src/commonMain/kotlin/commands/cli/ServeCommand.kt b/src/commonMain/kotlin/commands/cli/ServeCommand.kt new file mode 100644 index 0000000..22d3e6b --- /dev/null +++ b/src/commonMain/kotlin/commands/cli/ServeCommand.kt @@ -0,0 +1,79 @@ +package commands.cli + +import components.data.Repository +import components.data.TeamHistoryConfig +import history.storage.StoredHistory +import io.ktor.serialization.kotlinx.json.json +import io.ktor.server.application.Application +import io.ktor.server.application.ApplicationCall +import io.ktor.server.application.call +import io.ktor.server.application.install +import io.ktor.server.cio.CIO +import io.ktor.server.engine.BaseApplicationEngine +import io.ktor.server.engine.embeddedServer +import io.ktor.server.plugins.contentnegotiation.ContentNegotiation +import io.ktor.server.plugins.cors.routing.CORS +import io.ktor.server.plugins.forwardedheaders.ForwardedHeaders +import io.ktor.server.response.respond +import io.ktor.server.response.respondText +import io.ktor.server.routing.get +import io.ktor.server.routing.routing +import io.ktor.util.pipeline.PipelineContext +import kotlinx.coroutines.Runnable +import server.config.ServerConfig + +class ServeCommand( + private val teamHistoryConfig: TeamHistoryConfig, + private val storedHistory: StoredHistory, + private val serverConfig: ServerConfig, +) : Runnable { + + private lateinit var server: BaseApplicationEngine + private lateinit var storedRepos: List + + override fun run() { + println("== File configuration ==") + println(teamHistoryConfig.simpleFormat) + + println("\n== Serving ==") + storedRepos = storedHistory.fetchAllRepositories().map { + storedHistory.fetchRepository( + name = it.name, + includeCodeReviews = true, + includeDiscussions = true, + ) + } + if (storedRepos.isEmpty()) { + println("Aborted. No repositories found in the local storage.") + return + } + println("Now booting up a server...") + server = embeddedServer( + factory = CIO, + port = serverConfig.portApi, + ) { customizeConfiguration() } + + server.start(wait = true) + } + + private fun Application.customizeConfiguration() { + install(ForwardedHeaders) + install(CORS) { allowHost("localhost") } + install(ContentNegotiation) { json() } + + routing { + get("/") { respondToRoot() } + get("/shutdown") { respondToShutdown() } + } + } + + private suspend fun PipelineContext.respondToRoot() { + call.respond(storedRepos) + } + + private suspend fun PipelineContext.respondToShutdown() { + call.respondText("☠\uFE0F The server is dead now.") + server.stop() + } + +} diff --git a/src/commonMain/kotlin/commands/di/ManualModule.kt b/src/commonMain/kotlin/commands/di/ManualModule.kt new file mode 100644 index 0000000..6f82052 --- /dev/null +++ b/src/commonMain/kotlin/commands/di/ManualModule.kt @@ -0,0 +1,66 @@ +package commands.di + +import commands.cli.FetchCommand +import commands.cli.PrintCommand +import commands.cli.PurgeCommand +import commands.cli.ReportCommand +import commands.cli.ServeCommand +import components.data.TeamHistoryConfig +import history.TeamHistory +import history.github.di.provideGitHubHistory +import history.github.di.provideGitHubHistoryConfig +import history.storage.StoredHistory +import history.storage.config.StorageConfig +import history.storage.di.provideStorageConfig +import history.storage.di.provideStoredHistory +import server.config.ServerConfig +import server.di.provideServerConfig + +fun provideServeCommand( + teamHistoryConfig: TeamHistoryConfig, + storedHistory: StoredHistory = provideStoredHistory(teamHistoryConfig), + serverConfig: ServerConfig = provideServerConfig(), +) = ServeCommand( + teamHistoryConfig = teamHistoryConfig, + storedHistory = storedHistory, + serverConfig = serverConfig, +) + +fun providePurgeCommand( + teamHistoryConfig: TeamHistoryConfig, + storedHistory: StoredHistory = provideStoredHistory(teamHistoryConfig), +) = PurgeCommand( + teamHistoryConfig = teamHistoryConfig, + storedHistory = storedHistory, +) + +fun provideFetchCommand( + teamHistoryConfig: TeamHistoryConfig, + teamHistory: TeamHistory = provideGitHubHistory( + teamHistoryConfig = teamHistoryConfig, + gitHubHistoryConfig = provideGitHubHistoryConfig(), + ), + storageConfig: StorageConfig = provideStorageConfig(), + storedHistory: StoredHistory = provideStoredHistory(teamHistoryConfig), +) = FetchCommand( + teamHistoryConfig = teamHistoryConfig, + teamHistory = teamHistory, + storageConfig = storageConfig, + storedHistory = storedHistory, +) + +fun providePrintCommand( + teamHistoryConfig: TeamHistoryConfig, + storedHistory: StoredHistory = provideStoredHistory(teamHistoryConfig), +) = PrintCommand( + teamHistoryConfig = teamHistoryConfig, + storedHistory = storedHistory, +) + +fun provideReportCommand( + teamHistoryConfig: TeamHistoryConfig, + storedHistory: StoredHistory = provideStoredHistory(teamHistoryConfig), +) = ReportCommand( + teamHistoryConfig = teamHistoryConfig, + storedHistory = storedHistory, +) diff --git a/src/commonMain/kotlin/components/data/TeamHistoryConfig.kt b/src/commonMain/kotlin/components/data/TeamHistoryConfig.kt index 21f9a1d..08c2f48 100644 --- a/src/commonMain/kotlin/components/data/TeamHistoryConfig.kt +++ b/src/commonMain/kotlin/components/data/TeamHistoryConfig.kt @@ -33,8 +33,8 @@ data class TeamHistoryConfig( |Team History of $owner from $startDate to $endDate | · ${teams.size} teams |${teams.joinToString("\n") { it.simpleFormat(" · ") }} - | · Total: ${teams.sumOf { it.codeRepositories.size }} code repositories - | · Total: ${teams.sumOf { it.discussionRepositories.size }} discussion repositories + | · Code repositories: ${teams.sumOf { it.codeRepositories.size }} + | · Discussion repositories: ${teams.sumOf { it.discussionRepositories.size }} """.trimMargin() } diff --git a/src/commonMain/kotlin/components/metrics/PrintingUtils.kt b/src/commonMain/kotlin/components/metrics/PrintingUtils.kt index 500c411..ae015c9 100644 --- a/src/commonMain/kotlin/components/metrics/PrintingUtils.kt +++ b/src/commonMain/kotlin/components/metrics/PrintingUtils.kt @@ -21,7 +21,7 @@ fun Map.formatOutliersAsDuration( ) fun Map.formatOutliersAsCount( - cutAt: Int = 40, // characters + cutAt: Int = 50, // characters places: Int = 3, // top and bottom firstLinePrefix: String = " ·", // 6 spaces + bullet linePrefix: String = " $firstLinePrefix", // with 2 prefix spaces diff --git a/src/commonMain/kotlin/history/github/config/GitHubHistoryConfig.kt b/src/commonMain/kotlin/history/github/config/GitHubHistoryConfig.kt index fdf20a6..ba1dbdc 100644 --- a/src/commonMain/kotlin/history/github/config/GitHubHistoryConfig.kt +++ b/src/commonMain/kotlin/history/github/config/GitHubHistoryConfig.kt @@ -1,7 +1,14 @@ package history.github.config +import kotlin.math.roundToLong import utils.readEnvVar +private const val GITHUB_MAX_RPH = 15_000L // 15k requests per hour +private const val GITHUB_MAX_RPM = GITHUB_MAX_RPH / 60.0 // ~250 requests per minute +private const val GITHUB_MAX_RPS = GITHUB_MAX_RPM / 60.0 // ~4 requests per second +private const val PARALLEL_REQUESTS = 2L // 2 coroutines running in parallel +private val REQUEST_DELAY_DEFAULT = ((GITHUB_MAX_RPS / PARALLEL_REQUESTS) * 1000).roundToLong() // ~2100 ms delay + data class GitHubHistoryConfig( val baseRestUrl: String = readEnvVar("GITHUB_URL") @@ -28,12 +35,13 @@ data class GitHubHistoryConfig( ?: false, val shouldPrintProgress: Boolean = - readEnvVar("GITHUB_PROGRESS")?.toBoolean() + readEnvVar("GITHUB_PRINT_PROGRESS")?.toBoolean() ?: true, // https://docs.github.com/en/rest/overview/resources-in-the-rest-api?apiVersion=2022-11-28#rate-limits-for-requests-from-personal-accounts + val rateLimitDelayMillis: Long = readEnvVar("GITHUB_RATE_LIMIT_DELAY_MILLIS")?.toLongOrNull() ?.takeIf { it > 0 } - ?: (15_000 / 60 / 60 / 2), // 2 coroutines running, max 15k requests per hour + ?: REQUEST_DELAY_DEFAULT, ) diff --git a/src/commonMain/kotlin/server/config/ServerConfig.kt b/src/commonMain/kotlin/server/config/ServerConfig.kt new file mode 100644 index 0000000..855af80 --- /dev/null +++ b/src/commonMain/kotlin/server/config/ServerConfig.kt @@ -0,0 +1,10 @@ +package server.config + +import utils.readEnvVar + +data class ServerConfig( + val portApi: Int = + readEnvVar("STATS_PORT_API")?.toIntOrNull() + ?.takeIf { it in 1..65535 } + ?: 8080, +) diff --git a/src/commonMain/kotlin/server/di/ManualModule.kt b/src/commonMain/kotlin/server/di/ManualModule.kt new file mode 100644 index 0000000..76d9592 --- /dev/null +++ b/src/commonMain/kotlin/server/di/ManualModule.kt @@ -0,0 +1,5 @@ +package server.di + +import server.config.ServerConfig + +fun provideServerConfig(): ServerConfig = ServerConfig() diff --git a/src/commonMain/kotlin/utils/Utils.kt b/src/commonMain/kotlin/utils/Utils.kt index 98fddd9..ec7a41f 100644 --- a/src/commonMain/kotlin/utils/Utils.kt +++ b/src/commonMain/kotlin/utils/Utils.kt @@ -50,7 +50,7 @@ fun getLastMondayAsLocal(now: Instant = Clock.System.now()): LocalDate { fun TeamHistoryConfig.Companion.fromFile(path: String): TeamHistoryConfig { if (!path.endsWith(".yaml") && !path.endsWith(".yml")) - throw IllegalArgumentException("Must be a YAML file ($path).") + throw IllegalArgumentException("Must be a YAML file ($path).\n Found: $path") try { val ioPath = path.toPath(normalize = true) @@ -58,7 +58,7 @@ fun TeamHistoryConfig.Companion.fromFile(path: String): TeamHistoryConfig { val config = Yaml.Default.decodeFromString(fileContent) return config.sorted() } catch (e: Exception) { - throw IllegalArgumentException("Could not load config file ($path)", e) + throw IllegalArgumentException("Failed to load team config from $path.\n Error: ${e.message}", e) } }