diff --git a/.github/workflows/docs.yaml b/.github/workflows/docs.yaml index 80373d9..1b21458 100644 --- a/.github/workflows/docs.yaml +++ b/.github/workflows/docs.yaml @@ -27,7 +27,7 @@ jobs: - uses: actions/setup-java@v4 with: distribution: 'temurin' - java-version: '17' + java-version: '21' - uses: actions/cache@v4 with: path: | diff --git a/.github/workflows/testing.yaml b/.github/workflows/testing.yaml index 150baa1..380309d 100644 --- a/.github/workflows/testing.yaml +++ b/.github/workflows/testing.yaml @@ -8,6 +8,9 @@ on: branches: - main +permissions: + contents: read + jobs: gradle-test: runs-on: ubuntu-latest @@ -17,7 +20,7 @@ jobs: - uses: actions/setup-java@v4 with: distribution: 'temurin' - java-version: '17' + java-version: '21' - uses: actions/cache@v4 with: path: | diff --git a/.gitignore b/.gitignore index 017b388..e46661b 100644 --- a/.gitignore +++ b/.gitignore @@ -7,3 +7,4 @@ jte-classes/ logs/ ### Files ### +.envrc diff --git a/README.md b/README.md index ac0381b..2a7ed6a 100644 --- a/README.md +++ b/README.md @@ -1,11 +1,11 @@ # Kraken -![Java Version](https://img.shields.io/badge/Temurin-17-green?style=flat-square&logo=eclipse-adoptium) -![Kotlin Version](https://img.shields.io/badge/Kotlin-2.1.0-green?style=flat-square&logo=kotlin) +![Java Version](https://img.shields.io/badge/Temurin-21-green?style=flat-square&logo=eclipse-adoptium) +![Kotlin Version](https://img.shields.io/badge/Kotlin-2.2.0-green?style=flat-square&logo=kotlin) ![Status](https://img.shields.io/badge/Status-Beta-yellowgreen?style=flat-square) -[![Gradle](https://img.shields.io/badge/Gradle-8.12.0-informational?style=flat-square&logo=gradle)](https://github.com/gradle/gradle) -[![Ktlint](https://img.shields.io/badge/Ktlint-1.5.0-informational?style=flat-square)](https://github.com/pinterest/ktlint) +[![Gradle](https://img.shields.io/badge/Gradle-8.14.3-informational?style=flat-square&logo=gradle)](https://github.com/gradle/gradle) +[![Spotless](https://img.shields.io/badge/Spotless-7.1.0-informational?style=flat-square)](https://github.com/diffplug/spotless) [![Github - Version](https://img.shields.io/github/v/tag/Buried-In-Code/Kraken?logo=Github&label=Version&style=flat-square)](https://github.com/Buried-In-Code/Kraken/tags) [![Github - License](https://img.shields.io/github/license/Buried-In-Code/Kraken?logo=Github&label=License&style=flat-square)](https://opensource.org/licenses/MIT) @@ -30,7 +30,7 @@ Then, add Kraken as a dependency. ```kts dependencies { - implementation("com.github.Buried-In-Code:Kraken:0.2.3") + implementation("com.github.Buried-In-Code:Kraken:0.4.0") } ``` @@ -40,6 +40,7 @@ dependencies { import github.buriedincode.kraken.Metron import github.buriedincode.kraken.SQLiteCache import github.buriedincode.kraken.AuthenticationException +import github.buriedincode.kraken.RateLimitException import github.buriedincode.kraken.ServiceException fun main() { @@ -64,6 +65,8 @@ fun main() { } catch (ae: AuthenticationException) { println("Invalid Metron Username/Password.") + } catch(re: RatelimitException) { + println("Rate limit exceeded. Please try again later.") } catch (se: ServiceException) { println("Unsuccessful request: ${se.message}") } diff --git a/build.gradle.kts b/build.gradle.kts index 365effc..de80276 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -1,164 +1,176 @@ import com.github.benmanes.gradle.versions.updates.DependencyUpdatesTask import java.net.HttpURLConnection -import java.net.URL +import java.net.URI import java.nio.file.Files import java.nio.file.StandardOpenOption +import kotlin.io.path.absolutePathString +import kotlin.io.path.createDirectories +import kotlin.io.path.div plugins { - `java-library` - alias(libs.plugins.kotlin.jvm) - alias(libs.plugins.kotlinx.serialization) - alias(libs.plugins.dokka) - alias(libs.plugins.ktlint) - alias(libs.plugins.versions) - `maven-publish` + `java-library` + alias(libs.plugins.kotlin.jvm) + alias(libs.plugins.kotlinx.serialization) + alias(libs.plugins.dokka) + alias(libs.plugins.spotless) + alias(libs.plugins.versions) + `maven-publish` } println("Kotlin v${KotlinVersion.CURRENT}") + println("Java v${System.getProperty("java.version")}") + println("Arch: ${System.getProperty("os.arch")}") group = "github.buriedincode" -version = "0.3.1" + +version = "0.4.0" repositories { - mavenCentral() - mavenLocal() + mavenCentral() + mavenLocal() } dependencies { - implementation(libs.bundles.kotlinx.serialization) - implementation(libs.kotlin.logging) - runtimeOnly(libs.sqlite.jdbc) - testImplementation(libs.junit.jupiter) - testRuntimeOnly(libs.junit.platform.launcher) - testRuntimeOnly(libs.kotlin.reflect) - testRuntimeOnly(libs.log4j2.slf4j2.impl) -} + implementation(libs.bundles.kotlinx.serialization) + implementation(libs.kotlin.logging) -java { - toolchain { - languageVersion = JavaLanguageVersion.of(17) - } -} + runtimeOnly(libs.sqlite.jdbc) + + testImplementation(libs.junit.jupiter) -kotlin { - jvmToolchain(17) + testRuntimeOnly(libs.junit.platform.launcher) + testRuntimeOnly(libs.log4j2.slf4j2) } -configure { - version = "1.5.0" +java { toolchain { languageVersion = JavaLanguageVersion.of(21) } } + +kotlin { jvmToolchain(21) } + +spotless { + kotlin { + ktfmt().kotlinlangStyle().configure { + it.setMaxWidth(120) + it.setBlockIndent(2) + it.setContinuationIndent(2) + it.setRemoveUnusedImports(true) + it.setManageTrailingCommas(true) + } + } + kotlinGradle { + ktfmt().kotlinlangStyle().configure { + it.setMaxWidth(120) + it.setBlockIndent(2) + it.setContinuationIndent(2) + it.setRemoveUnusedImports(true) + it.setManageTrailingCommas(true) + } + } } tasks.test { - environment("METRON__USERNAME", System.getenv("METRON__USERNAME")) - environment("METRON__PASSWORD", System.getenv("METRON__PASSWORD")) - useJUnitPlatform() - testLogging { - events("passed", "skipped", "failed") - } + environment("METRON__USERNAME", System.getenv("METRON__USERNAME")) + environment("METRON__PASSWORD", System.getenv("METRON__PASSWORD")) + useJUnitPlatform() + testLogging { events("passed", "skipped", "failed") } } fun isNonStable(version: String): Boolean { - val stableKeyword = listOf("RELEASE", "FINAL", "GA").any { version.uppercase().contains(it) } - val regex = "^[0-9,.v-]+(-r)?$".toRegex() - val isStable = stableKeyword || regex.matches(version) - return isStable.not() + val stableKeyword = listOf("RELEASE", "FINAL", "GA").any { version.uppercase().contains(it) } + val regex = "^[0-9,.v-]+(-r)?$".toRegex() + val isStable = stableKeyword || regex.matches(version) + return isStable.not() } tasks.withType { - gradleReleaseChannel = "current" - resolutionStrategy { - componentSelection { - all { - if (isNonStable(candidate.version) && !isNonStable(currentVersion)) { - reject("Release candidate") - } - } + gradleReleaseChannel = "current" + checkForGradleUpdate = true + checkConstraints = false + checkBuildEnvironmentConstraints = false + resolutionStrategy { + componentSelection { + all { + if (isNonStable(candidate.version) && !isNonStable(currentVersion)) { + reject("Release candidate") } + } } + } } -publishing { - publications { - create("kraken") { - from(components["java"]) - } - } -} +publishing { publications { create("kraken") { from(components["java"]) } } } tasks.register("processReadme") { - group = "documentation" - description = "Processes the README.md file to inline SVG badges." + group = "documentation" + description = "Processes the README.md file to inline SVG badges." - doLast { - val linkedBadgePattern = """\[\!\[(.*?)\]\((.*?)\)\]\((.*?)\)""".toRegex() // [![alt](url)](link) - val badgePattern = """\!\[(.*?)\]\((.*?)\)""".toRegex() // ![alt](url) + doLast { + val linkedBadgePattern = """\[\!\[(.*?)\]\((.*?)\)\]\((.*?)\)""".toRegex() // [![alt](url)](link) + val badgePattern = """\!\[(.*?)\]\((.*?)\)""".toRegex() // ![alt](url) - val inputPath = project.rootDir.toPath().resolve("README.md") - val outputPath = project.buildDir.toPath().resolve("Processed-README.md") + val inputPath = project.rootDir.toPath() / "README.md" + val outputPath = project.layout.buildDirectory.get().asFile.toPath() / "Processed-README.md" - if (!Files.exists(inputPath)) { - throw IllegalStateException("${inputPath.toAbsolutePath()} not found.") - } + if (!Files.exists(inputPath)) { + throw IllegalStateException("${inputPath.absolutePathString()} not found.") + } - var content = Files.readAllLines(inputPath).joinToString("\n") - content = content.replaceFirst("# Kraken", "# Module Kraken") - - fun fetchSvg(url: String): String? { - return try { - val connection = URL(url).openConnection() as HttpURLConnection - connection.requestMethod = "GET" - connection.connect() - - if (connection.responseCode == 200 && connection.contentType.contains("image/svg+xml")) { - connection.inputStream.bufferedReader().use { it.readText() } - } else { - println("Warning: $url is not an SVG badge") - null - } - } catch (e: Exception) { - println("Error fetching $url: ${e.message}") - null - } - } + var content = Files.readAllLines(inputPath).joinToString("\n") + content = content.replaceFirst("# Kraken", "# Module Kraken") - fun processContent(pattern: Regex, replaceFunction: (MatchResult) -> String): String { - return pattern.replace(content) { match -> - replaceFunction(match) - } - } + fun fetchSvg(url: String): String? { + return try { + val connection = URI.create(url).toURL().openConnection() as HttpURLConnection + connection.requestMethod = "GET" + connection.connect() - content = processContent(linkedBadgePattern) { match -> - val altText = match.groupValues[1] - val badgeUrl = match.groupValues[2] - val linkUrl = match.groupValues[3] - val svgContent = fetchSvg(badgeUrl) - if (svgContent != null) { - """$svgContent""" - } else { - """$altText""" - } - } - - content = processContent(badgePattern) { match -> - val altText = match.groupValues[1] - val badgeUrl = match.groupValues[2] - val svgContent = fetchSvg(badgeUrl) - svgContent ?: """$altText""" + if (connection.responseCode == 200 && connection.contentType.contains("image/svg+xml")) { + connection.inputStream.bufferedReader().use { it.readText() } + } else { + println("Warning: $url is not an SVG badge") + null } + } catch (e: Exception) { + println("Error fetching $url: ${e.message}") + null + } + } - Files.createDirectories(outputPath.parent) - Files.writeString(outputPath, content, StandardOpenOption.CREATE, StandardOpenOption.TRUNCATE_EXISTING) - println("Processing complete. Output written to ${outputPath.toAbsolutePath()}") + fun processContent(pattern: Regex, replaceFunction: (MatchResult) -> String): String { + return pattern.replace(content) { match -> replaceFunction(match) } } + + content = + processContent(linkedBadgePattern) { match -> + val altText = match.groupValues[1] + val badgeUrl = match.groupValues[2] + val linkUrl = match.groupValues[3] + val svgContent = fetchSvg(badgeUrl) + if (svgContent != null) { + """$svgContent""" + } else { + """$altText""" + } + } + + content = + processContent(badgePattern) { match -> + val altText = match.groupValues[1] + val badgeUrl = match.groupValues[2] + val svgContent = fetchSvg(badgeUrl) + svgContent ?: """$altText""" + } + + outputPath.parent.createDirectories() + Files.writeString(outputPath, content, StandardOpenOption.CREATE, StandardOpenOption.TRUNCATE_EXISTING) + println("Processing complete. Output written to ${outputPath.absolutePathString()}") + } } tasks.dokkaHtml { - dependsOn("processReadme") - dokkaSourceSets { - configureEach { - includes.from(project.buildDir.toPath().resolve("Processed-README.md")) - } - } + dependsOn("processReadme") + dokkaSourceSets { + configureEach { includes.from(project.layout.buildDirectory.get().asFile.toPath() / "Processed-README.md") } + } } diff --git a/cache.sqlite b/cache.sqlite index 40f4cd3..0a9931e 100644 Binary files a/cache.sqlite and b/cache.sqlite differ diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 9d3e667..daf17ac 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -5,18 +5,17 @@ kotlin = "2.2.0" dokka = { id = "org.jetbrains.dokka", version = "2.0.0" } kotlin-jvm = { id = "org.jetbrains.kotlin.jvm", version.ref = "kotlin" } kotlinx-serialization = { id = "org.jetbrains.kotlin.plugin.serialization", version.ref = "kotlin" } -ktlint = { id = "org.jlleitschuh.gradle.ktlint", version = "13.0.0" } +spotless = { id = "com.diffplug.spotless", version = "7.1.0" } versions = { id = "com.github.ben-manes.versions", version = "0.52.0" } [libraries] -junit-jupiter = { group = "org.junit.jupiter", name = "junit-jupiter", version = "5.13.3" } -junit-platform-launcher = { group = "org.junit.platform", name = "junit-platform-launcher" } -kotlin-logging = { group = "io.github.oshai", name = "kotlin-logging-jvm", version = "7.0.7" } -kotlin-reflect = { group = "org.jetbrains.kotlin", name = "kotlin-reflect", version.ref = "kotlin" } -kotlinx-datetime = { group = "org.jetbrains.kotlinx", name = "kotlinx-datetime", version = "0.7.1-0.6.x-compat" } -kotlinx-serialization-json = { group = "org.jetbrains.kotlinx", name = "kotlinx-serialization-json", version = "1.9.0" } -log4j2-slf4j2-impl = { group = "org.apache.logging.log4j", name = "log4j-slf4j2-impl", version = "2.25.1" } -sqlite-jdbc = { group = "org.xerial", name = "sqlite-jdbc", version = "3.50.2.0" } +junit-jupiter = { module = "org.junit.jupiter:junit-jupiter", version = "5.13.3" } +junit-platform-launcher = { module = "org.junit.platform:junit-platform-launcher", version = "1.13.3" } +kotlin-logging = { module = "io.github.oshai:kotlin-logging-jvm", version = "7.0.7" } +kotlinx-datetime = { module = "org.jetbrains.kotlinx:kotlinx-datetime", version = "0.7.1" } +kotlinx-serialization-json = { module = "org.jetbrains.kotlinx:kotlinx-serialization-json", version = "1.9.0" } +log4j2-slf4j2 = { module = "org.apache.logging.log4j:log4j-slf4j2-impl", version = "2.25.1" } +sqlite-jdbc = { module = "org.xerial:sqlite-jdbc", version = "3.50.2.0" } [bundles] kotlinx-serialization = ["kotlinx-serialization-json", "kotlinx-datetime"] diff --git a/gradle/wrapper/gradle-wrapper.jar b/gradle/wrapper/gradle-wrapper.jar index a4b76b9..9bbc975 100644 Binary files a/gradle/wrapper/gradle-wrapper.jar and b/gradle/wrapper/gradle-wrapper.jar differ diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index 37f853b..d4081da 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -1,6 +1,6 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-8.13-bin.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-8.14.3-bin.zip networkTimeout=10000 validateDistributionUrl=true zipStoreBase=GRADLE_USER_HOME diff --git a/gradlew b/gradlew index f3b75f3..faf9300 100755 --- a/gradlew +++ b/gradlew @@ -205,7 +205,7 @@ fi DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' # Collect all arguments for the java command: -# * DEFAULT_JVM_OPTS, JAVA_OPTS, JAVA_OPTS, and optsEnvironmentVar are not allowed to contain shell fragments, +# * DEFAULT_JVM_OPTS, JAVA_OPTS, and optsEnvironmentVar are not allowed to contain shell fragments, # and any embedded shellness will be escaped. # * For example: A user cannot expect ${Hostname} to be expanded, as it is an environment variable and will be # treated as '${Hostname}' itself on the command line. diff --git a/jitpack.yml b/jitpack.yml index ee69e40..255e0f4 100644 --- a/jitpack.yml +++ b/jitpack.yml @@ -1,5 +1,5 @@ jdk: - - openjdk17 + - openjdk21 before_install: - - sdk install java 17-open - - sdk use java 17-open + - sdk install java 21-open + - sdk use java 21-open diff --git a/settings.gradle.kts b/settings.gradle.kts index de99075..e5afcd2 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -1,5 +1,3 @@ -plugins { - id("org.gradle.toolchains.foojay-resolver-convention") version "1.0.0" -} +plugins { id("org.gradle.toolchains.foojay-resolver-convention") version "1.0.0" } rootProject.name = "Kraken" diff --git a/src/main/kotlin/github/buriedincode/kraken/Metron.kt b/src/main/kotlin/github/buriedincode/kraken/Metron.kt index 8d840d7..ae1d4f6 100644 --- a/src/main/kotlin/github/buriedincode/kraken/Metron.kt +++ b/src/main/kotlin/github/buriedincode/kraken/Metron.kt @@ -16,12 +16,6 @@ import github.buriedincode.kraken.schemas.Team import github.buriedincode.kraken.schemas.Universe import io.github.oshai.kotlinlogging.KotlinLogging import io.github.oshai.kotlinlogging.Level -import kotlinx.serialization.ExperimentalSerializationApi -import kotlinx.serialization.SerializationException -import kotlinx.serialization.json.Json -import kotlinx.serialization.json.JsonNamingStrategy -import kotlinx.serialization.json.jsonObject -import kotlinx.serialization.json.jsonPrimitive import java.io.IOException import java.net.URI import java.net.URLEncoder @@ -32,416 +26,413 @@ import java.net.http.HttpResponse import java.nio.charset.StandardCharsets import java.time.Duration import java.util.Base64 -import kotlin.collections.joinToString -import kotlin.collections.plus -import kotlin.collections.sortedBy -import kotlin.jvm.Throws -import kotlin.ranges.until -import kotlin.text.isNotEmpty -import kotlin.text.toByteArray -import kotlin.text.toInt +import kotlin.jvm.optionals.getOrNull +import kotlinx.serialization.ExperimentalSerializationApi +import kotlinx.serialization.SerializationException +import kotlinx.serialization.json.Json +import kotlinx.serialization.json.JsonNamingStrategy +import kotlinx.serialization.json.jsonObject +import kotlinx.serialization.json.jsonPrimitive /** * A client for interacting with the Metron API. * - * @constructor Creates a new instance of the `Metron` client. * @param username The username for authentication with the Metron API. * @param password The password for authentication with the Metron API. * @param cache An optional [SQLiteCache] instance for caching API responses. Defaults to `null` (no caching). * @param timeout The maximum duration for HTTP connections. Defaults to 30 seconds. * @param maxRetries The maximum number of retries for requests that fail due to rate-limiting. Defaults to 5. - * * @property cache The optional cache instance used for storing and retrieving cached API responses. * @property maxRetries The maximum number of retries allowed for rate-limited requests. + * @constructor Creates a new instance of the `Metron` client. */ class Metron( - username: String, - password: String, - val cache: SQLiteCache? = null, - timeout: Duration = Duration.ofSeconds(30), - var maxRetries: Int = 5, + username: String, + password: String, + val cache: SQLiteCache? = null, + timeout: Duration = Duration.ofSeconds(30), + var maxRetries: Int = 5, ) { - private val client: HttpClient = HttpClient - .newBuilder() - .followRedirects(HttpClient.Redirect.ALWAYS) - .connectTimeout(timeout) - .build() - private val authorization: String = "Basic " + Base64.getEncoder().encodeToString("$username:$password".toByteArray()) - - @Throws(ServiceException::class, AuthenticationException::class, RateLimitException::class) - private fun performGetRequest(uri: URI): String { - var attempt = 0 - var backoffDelay = 2000L - - while (attempt < this.maxRetries) { - try { - @Suppress("ktlint:standard:max-line-length", "ktlint:standard:argument-list-wrapping") - val request = HttpRequest - .newBuilder() - .uri(uri) - .setHeader("Accept", "application/json") - .setHeader("User-Agent", "Kraken/$VERSION (${System.getProperty("os.name")}/${System.getProperty("os.version")}; Kotlin/${KotlinVersion.CURRENT})") - .setHeader("Authorization", this.authorization) - .GET() - .build() - val response = this.client.send(request, HttpResponse.BodyHandlers.ofString()) - val level = when (response.statusCode()) { - in 100 until 200 -> Level.WARN - in 200 until 300 -> Level.DEBUG - in 300 until 400 -> Level.INFO - in 400 until 500 -> Level.WARN - else -> Level.ERROR - } - LOGGER.log(level) { "GET: ${response.statusCode()} - $uri" } - if (response.statusCode() == 200) { - return response.body() - } else if (response.statusCode() == 429) { - LOGGER.warn { "Received 429 Too Many Requests. Retrying in ${backoffDelay}ms..." } - Thread.sleep(backoffDelay) - backoffDelay *= 2 - attempt++ - continue - } - - val content = JSON.parseToJsonElement(response.body()).jsonObject - LOGGER.error { content.toString() } - throw when (response.statusCode()) { - 401 -> AuthenticationException(content["detail"]?.jsonPrimitive?.content ?: "") - 404 -> ServiceException("Resource not found") - else -> ServiceException(content["detail"]?.jsonPrimitive?.content ?: "") - } - } catch (ioe: IOException) { - throw ServiceException(cause = ioe) - } catch (hcte: HttpConnectTimeoutException) { - throw ServiceException(cause = hcte) - } catch (ie: InterruptedException) { - throw ServiceException(cause = ie) - } catch (se: SerializationException) { - throw ServiceException(cause = se) - } - attempt++ + private val client: HttpClient = + HttpClient.newBuilder().followRedirects(HttpClient.Redirect.ALWAYS).connectTimeout(timeout).build() + private val authorization: String = "Basic " + Base64.getEncoder().encodeToString("$username:$password".toByteArray()) + + @Throws(ServiceException::class, AuthenticationException::class, RateLimitException::class) + private fun performGetRequest(uri: URI): String { + var attempt = 0 + + while (attempt < this.maxRetries) { + try { + val request = + HttpRequest.newBuilder() + .uri(uri) + .setHeader("Accept", "application/json") + .setHeader( + "User-Agent", + "Kraken/$VERSION (${System.getProperty("os.name")}/${System.getProperty("os.version")}; Kotlin/${KotlinVersion.CURRENT})", + ) + .setHeader("Authorization", this.authorization) + .GET() + .build() + val response = this.client.send(request, HttpResponse.BodyHandlers.ofString()) + val level = + when (response.statusCode()) { + in 100 until 200 -> Level.WARN + in 200 until 300 -> Level.DEBUG + in 300 until 400 -> Level.INFO + in 400 until 500 -> Level.WARN + else -> Level.ERROR + } + LOGGER.log(level) { "GET: ${response.statusCode()} - $uri" } + if (response.statusCode() == 200) { + return response.body() + } else if (response.statusCode() == 429) { + val backoffDelay = response.headers().firstValue("Retry-After").getOrNull()?.toLongOrNull() ?: 2 + LOGGER.warn { "Received 429 Too Many Requests. Retrying in ${backoffDelay}s..." } + Thread.sleep(backoffDelay * 1000) + attempt++ + continue } - throw RateLimitException("Max retries reached for $uri") - } - @Throws(ServiceException::class, AuthenticationException::class, RateLimitException::class) - internal inline fun getRequest(uri: URI): T { - this.cache?.select(url = uri.toString())?.let { - try { - LOGGER.debug { "Using cached response for $uri" } - return JSON.decodeFromString(it) - } catch (se: SerializationException) { - LOGGER.warn(se) { "Unable to deserialize cached response" } - this.cache.delete(url = uri.toString()) - } - } - val response = performGetRequest(uri = uri) - this.cache?.insert(url = uri.toString(), response = response) - return try { - JSON.decodeFromString(response) - } catch (se: SerializationException) { - throw ServiceException(cause = se) + val content = JSON.parseToJsonElement(response.body()).jsonObject + LOGGER.error { content.toString() } + throw when (response.statusCode()) { + 401 -> AuthenticationException(content["detail"]?.jsonPrimitive?.content ?: "") + 404 -> ServiceException("Resource not found") + else -> ServiceException(content["detail"]?.jsonPrimitive?.content ?: "") } + } catch (ioe: IOException) { + throw ServiceException(cause = ioe) + } catch (hcte: HttpConnectTimeoutException) { + throw ServiceException(cause = hcte) + } catch (ie: InterruptedException) { + throw ServiceException(cause = ie) + } catch (se: SerializationException) { + throw ServiceException(cause = se) + } } - - internal fun encodeURI(endpoint: String, params: Map = emptyMap()): URI { - val encodedParams = params.entries - .sortedBy { it.key } - .joinToString("&") { "${it.key}=${URLEncoder.encode(it.value, StandardCharsets.UTF_8)}" } - return URI.create("$BASE_API$endpoint/${if (encodedParams.isNotEmpty()) "?$encodedParams" else ""}") + throw RateLimitException("Max retries reached for $uri") + } + + @Throws(ServiceException::class, AuthenticationException::class, RateLimitException::class) + internal inline fun getRequest(uri: URI): T { + this.cache?.select(url = uri.toString())?.let { + try { + LOGGER.debug { "Using cached response for $uri" } + return JSON.decodeFromString(it) + } catch (se: SerializationException) { + LOGGER.warn(se) { "Unable to deserialize cached response" } + this.cache.delete(url = uri.toString()) + } } - - @Throws(ServiceException::class, AuthenticationException::class, RateLimitException::class) - internal inline fun fetchList(endpoint: String, params: Map): List { - val resultList = mutableListOf() - var page = params.getOrDefault("page", "1").toInt() - - do { - val uri = encodeURI(endpoint = endpoint, params = params + ("page" to page.toString())) - val response = getRequest>(uri = uri) - resultList.addAll(response.results) - page++ - } while (response.next != null) - - return resultList - } - - @Throws(ServiceException::class, AuthenticationException::class, RateLimitException::class) - internal inline fun fetchItem(endpoint: String): T = getRequest(uri = this.encodeURI(endpoint = endpoint)) - - /** - * Retrieves a list of Arcs from the Metron API. - * - * @param params A map of query parameters to filter the results. - * @return A list of Arcs as [BaseResource] objects. - * @throws ServiceException If a generic error occurs during the API call. - * @throws AuthenticationException If the provided credentials are invalid. - * @throws RateLimitException If the maximum number of retries is exceeded due to rate-limiting. - */ - @Throws(ServiceException::class, AuthenticationException::class, RateLimitException::class) - fun listArcs(params: Map = emptyMap()): List { - return fetchList(endpoint = "/arc", params = params) + val response = performGetRequest(uri = uri) + this.cache?.insert(url = uri.toString(), response = response) + return try { + JSON.decodeFromString(response) + } catch (se: SerializationException) { + throw ServiceException(cause = se) } - - /** - * Retrieves details of a specific Arc by its ID. - * - * @param id The unique identifier of the Arc to retrieve. - * @return The Arc as an [Arc] object. - * @throws ServiceException If a generic error occurs during the API call. - * @throws AuthenticationException If the provided credentials are invalid. - * @throws RateLimitException If the maximum number of retries is exceeded due to rate-limiting. - */ - @Throws(ServiceException::class, AuthenticationException::class, RateLimitException::class) - fun getArc(id: Long): Arc = fetchItem(endpoint = "/arc/$id") - - /** - * Retrieves a list of Characters from the Metron API. - * - * @param params A map of query parameters to filter the results. - * @return A list of Characters as [BaseResource] objects. - * @throws ServiceException If a generic error occurs during the API call. - * @throws AuthenticationException If the provided credentials are invalid. - * @throws RateLimitException If the maximum number of retries is exceeded due to rate-limiting. - */ - @Throws(ServiceException::class, AuthenticationException::class, RateLimitException::class) - fun listCharacters(params: Map = emptyMap()): List { - return fetchList(endpoint = "/character", params = params) - } - - /** - * Retrieves details of a specific Character by its ID. - * - * @param id The unique identifier of the Character to retrieve. - * @return The Character as a [Character] object. - * @throws ServiceException If a generic error occurs during the API call. - * @throws AuthenticationException If the provided credentials are invalid. - * @throws RateLimitException If the maximum number of retries is exceeded due to rate-limiting. - */ - @Throws(ServiceException::class, AuthenticationException::class, RateLimitException::class) - fun getCharacter(id: Long): Character = fetchItem(endpoint = "/character/$id") - - /** - * Retrieves a list of Creators from the Metron API. - * - * @param params A map of query parameters to filter the results. - * @return A list of Creators as [BaseResource] objects. - * @throws ServiceException If a generic error occurs during the API call. - * @throws AuthenticationException If the provided credentials are invalid. - * @throws RateLimitException If the maximum number of retries is exceeded due to rate-limiting. - */ - @Throws(ServiceException::class, AuthenticationException::class, RateLimitException::class) - fun listCreators(params: Map = emptyMap()): List { - return fetchList(endpoint = "/creator", params = params) - } - - /** - * Retrieves details of a specific Creator by its ID. - * - * @param id The unique identifier of the Creator to retrieve. - * @return The Creator as a [Creator] object. - * @throws ServiceException If a generic error occurs during the API call. - * @throws AuthenticationException If the provided credentials are invalid. - * @throws RateLimitException If the maximum number of retries is exceeded due to rate-limiting. - */ - @Throws(ServiceException::class, AuthenticationException::class, RateLimitException::class) - fun getCreator(id: Long): Creator = fetchItem(endpoint = "/creator/$id") - - /** - * Retrieves a list of Imprints from the Metron API. - * - * @param params A map of query parameters to filter the results. - * @return A list of Imprints as [BaseResource] objects. - * @throws ServiceException If a generic error occurs during the API call. - * @throws AuthenticationException If the provided credentials are invalid. - * @throws RateLimitException If the maximum number of retries is exceeded due to rate-limiting. - */ - @Throws(ServiceException::class, AuthenticationException::class, RateLimitException::class) - fun listImprints(params: Map = emptyMap()): List { - return fetchList(endpoint = "/imprint", params = params) - } - - /** - * Retrieves details of a specific Imprint by its ID. - * - * @param id The unique identifier of the Imprint to retrieve. - * @return The Imprint as an [Imprint] object. - * @throws ServiceException If a generic error occurs during the API call. - * @throws AuthenticationException If the provided credentials are invalid. - * @throws RateLimitException If the maximum number of retries is exceeded due to rate-limiting. - */ - @Throws(ServiceException::class, AuthenticationException::class, RateLimitException::class) - fun getImprint(id: Long): Imprint = fetchItem(endpoint = "/imprint/$id") - - /** - * Retrieves a list of Issues from the Metron API. - * - * @param params A map of query parameters to filter the results. - * @return A list of Issues as [BasicIssue] objects. - * @throws ServiceException If a generic error occurs during the API call. - * @throws AuthenticationException If the provided credentials are invalid. - * @throws RateLimitException If the maximum number of retries is exceeded due to rate-limiting. - */ - @Throws(ServiceException::class, AuthenticationException::class, RateLimitException::class) - fun listIssues(params: Map = emptyMap()): List { - return fetchList(endpoint = "/issue", params = params) - } - - /** - * Retrieves details of a specific Issue by its ID. - * - * @param id The unique identifier of the Issue to retrieve. - * @return The Issue as an [Issue] object. - * @throws ServiceException If a generic error occurs during the API call. - * @throws AuthenticationException If the provided credentials are invalid. - * @throws RateLimitException If the maximum number of retries is exceeded due to rate-limiting. - */ - @Throws(ServiceException::class, AuthenticationException::class, RateLimitException::class) - fun getIssue(id: Long): Issue = fetchItem(endpoint = "/issue/$id") - - /** - * Retrieves a list of Publishers from the Metron API. - * - * @param params A map of query parameters to filter the results. - * @return A list of Publishers as [BaseResource] objects. - * @throws ServiceException If a generic error occurs during the API call. - * @throws AuthenticationException If the provided credentials are invalid. - * @throws RateLimitException If the maximum number of retries is exceeded due to rate-limiting. - */ - @Throws(ServiceException::class, AuthenticationException::class, RateLimitException::class) - fun listPublishers(params: Map = emptyMap()): List { - return fetchList(endpoint = "/publisher", params = params) - } - - /** - * Retrieves details of a specific Publisher by its ID. - * - * @param id The unique identifier of the Publisher to retrieve. - * @return The Publisher as a [Publisher] object. - * @throws ServiceException If a generic error occurs during the API call. - * @throws AuthenticationException If the provided credentials are invalid. - * @throws RateLimitException If the maximum number of retries is exceeded due to rate-limiting. - */ - @Throws(ServiceException::class, AuthenticationException::class, RateLimitException::class) - fun getPublisher(id: Long): Publisher = fetchItem(endpoint = "/publisher/$id") - - /** - * Retrieves a list of Roles from the Metron API. - * - * @param params A map of query parameters to filter the results. - * @return A list of Roles as [GenericItem] objects. - * @throws ServiceException If a generic error occurs during the API call. - * @throws AuthenticationException If the provided credentials are invalid. - * @throws RateLimitException If the maximum number of retries is exceeded due to rate-limiting. - */ - @Throws(ServiceException::class, AuthenticationException::class, RateLimitException::class) - fun listRoles(params: Map = emptyMap()): List { - return fetchList(endpoint = "/role", params = params) - } - - /** - * Retrieves a list of Series from the Metron API. - * - * @param params A map of query parameters to filter the results. - * @return A list of Series as [BasicSeries] objects. - * @throws ServiceException If a generic error occurs during the API call. - * @throws AuthenticationException If the provided credentials are invalid. - * @throws RateLimitException If the maximum number of retries is exceeded due to rate-limiting. - */ - @Throws(ServiceException::class, AuthenticationException::class, RateLimitException::class) - fun listSeries(params: Map = emptyMap()): List { - return fetchList(endpoint = "/series", params = params) - } - - /** - * Retrieves details of a specific Series by its ID. - * - * @param id The unique identifier of the Series to retrieve. - * @return The Series as a [Series] object. - * @throws ServiceException If a generic error occurs during the API call. - * @throws AuthenticationException If the provided credentials are invalid. - * @throws RateLimitException If the maximum number of retries is exceeded due to rate-limiting. - */ - @Throws(ServiceException::class, AuthenticationException::class, RateLimitException::class) - fun getSeries(id: Long): Series = fetchItem(endpoint = "/series/$id") - - /** - * Retrieves a list of SeriesTypes from the Metron API. - * - * @param params A map of query parameters to filter the results. - * @return A list of SeriesTypes as [GenericItem] objects. - * @throws ServiceException If a generic error occurs during the API call. - * @throws AuthenticationException If the provided credentials are invalid. - * @throws RateLimitException If the maximum number of retries is exceeded due to rate-limiting. - */ - @Throws(ServiceException::class, AuthenticationException::class, RateLimitException::class) - fun listSeriesTypes(params: Map = emptyMap()): List { - return fetchList(endpoint = "/series_type", params = params) - } - - /** - * Retrieves a list of Teams from the Metron API. - * - * @param params A map of query parameters to filter the results. - * @return A list of Teams as [BaseResource] objects. - * @throws ServiceException If a generic error occurs during the API call. - * @throws AuthenticationException If the provided credentials are invalid. - * @throws RateLimitException If the maximum number of retries is exceeded due to rate-limiting. - */ - @Throws(ServiceException::class, AuthenticationException::class, RateLimitException::class) - fun listTeams(params: Map = emptyMap()): List { - return fetchList(endpoint = "/team", params = params) - } - - /** - * Retrieves details of a specific Team by its ID. - * - * @param id The unique identifier of the Team to retrieve. - * @return The Team as a [Team] object. - * @throws ServiceException If a generic error occurs during the API call. - * @throws AuthenticationException If the provided credentials are invalid. - * @throws RateLimitException If the maximum number of retries is exceeded due to rate-limiting. - */ - @Throws(ServiceException::class, AuthenticationException::class, RateLimitException::class) - fun getTeam(id: Long): Team = fetchItem(endpoint = "/team/$id") - - /** - * Retrieves a list of Universes from the Metron API. - * - * @param params A map of query parameters to filter the results. - * @return A list of Universes as [BaseResource] objects. - * @throws ServiceException If a generic error occurs during the API call. - * @throws AuthenticationException If the provided credentials are invalid. - * @throws RateLimitException If the maximum number of retries is exceeded due to rate-limiting. - */ - @Throws(ServiceException::class, AuthenticationException::class, RateLimitException::class) - fun listUniverses(params: Map = emptyMap()): List { - return fetchList(endpoint = "/universe", params = params) - } - - /** - * Retrieves details of a specific Universe by its ID. - * - * @param id The unique identifier of the Universe to retrieve. - * @return The Universe as an [Universe] object. - * @throws ServiceException If a generic error occurs during the API call. - * @throws AuthenticationException If the provided credentials are invalid. - * @throws RateLimitException If the maximum number of retries is exceeded due to rate-limiting. - */ - @Throws(ServiceException::class, AuthenticationException::class, RateLimitException::class) - fun getUniverse(id: Long): Universe = fetchItem(endpoint = "/universe/$id") - - companion object { - @JvmStatic - private val LOGGER = KotlinLogging.logger { } - - private const val BASE_API = "https://metron.cloud/api" - - @OptIn(ExperimentalSerializationApi::class) - private val JSON: Json = Json { - prettyPrint = true - encodeDefaults = true - namingStrategy = JsonNamingStrategy.SnakeCase - } + } + + internal fun encodeURI(endpoint: String, params: Map = emptyMap()): URI { + val encodedParams = + params.entries + .sortedBy { it.key } + .joinToString("&") { "${it.key}=${URLEncoder.encode(it.value, StandardCharsets.UTF_8)}" } + return URI.create("$BASE_API$endpoint/${if (encodedParams.isNotEmpty()) "?$encodedParams" else ""}") + } + + @Throws(ServiceException::class, AuthenticationException::class, RateLimitException::class) + internal inline fun fetchList(endpoint: String, params: Map): List { + val resultList = mutableListOf() + var page = params.getOrDefault("page", "1").toInt() + + do { + val uri = encodeURI(endpoint = endpoint, params = params + ("page" to page.toString())) + val response = getRequest>(uri = uri) + resultList.addAll(response.results) + page++ + } while (response.next != null) + + return resultList + } + + @Throws(ServiceException::class, AuthenticationException::class, RateLimitException::class) + internal inline fun fetchItem(endpoint: String): T = + getRequest(uri = this.encodeURI(endpoint = endpoint)) + + /** + * Retrieves a list of Arcs from the Metron API. + * + * @param params A map of query parameters to filter the results. + * @return A list of Arcs as [BaseResource] objects. + * @throws ServiceException If a generic error occurs during the API call. + * @throws AuthenticationException If the provided credentials are invalid. + * @throws RateLimitException If the maximum number of retries is exceeded due to rate-limiting. + */ + @Throws(ServiceException::class, AuthenticationException::class, RateLimitException::class) + fun listArcs(params: Map = emptyMap()): List { + return fetchList(endpoint = "/arc", params = params) + } + + /** + * Retrieves details of a specific Arc by its ID. + * + * @param id The unique identifier of the Arc to retrieve. + * @return The Arc as an [Arc] object. + * @throws ServiceException If a generic error occurs during the API call. + * @throws AuthenticationException If the provided credentials are invalid. + * @throws RateLimitException If the maximum number of retries is exceeded due to rate-limiting. + */ + @Throws(ServiceException::class, AuthenticationException::class, RateLimitException::class) + fun getArc(id: Long): Arc = fetchItem(endpoint = "/arc/$id") + + /** + * Retrieves a list of Characters from the Metron API. + * + * @param params A map of query parameters to filter the results. + * @return A list of Characters as [BaseResource] objects. + * @throws ServiceException If a generic error occurs during the API call. + * @throws AuthenticationException If the provided credentials are invalid. + * @throws RateLimitException If the maximum number of retries is exceeded due to rate-limiting. + */ + @Throws(ServiceException::class, AuthenticationException::class, RateLimitException::class) + fun listCharacters(params: Map = emptyMap()): List { + return fetchList(endpoint = "/character", params = params) + } + + /** + * Retrieves details of a specific Character by its ID. + * + * @param id The unique identifier of the Character to retrieve. + * @return The Character as a [Character] object. + * @throws ServiceException If a generic error occurs during the API call. + * @throws AuthenticationException If the provided credentials are invalid. + * @throws RateLimitException If the maximum number of retries is exceeded due to rate-limiting. + */ + @Throws(ServiceException::class, AuthenticationException::class, RateLimitException::class) + fun getCharacter(id: Long): Character = fetchItem(endpoint = "/character/$id") + + /** + * Retrieves a list of Creators from the Metron API. + * + * @param params A map of query parameters to filter the results. + * @return A list of Creators as [BaseResource] objects. + * @throws ServiceException If a generic error occurs during the API call. + * @throws AuthenticationException If the provided credentials are invalid. + * @throws RateLimitException If the maximum number of retries is exceeded due to rate-limiting. + */ + @Throws(ServiceException::class, AuthenticationException::class, RateLimitException::class) + fun listCreators(params: Map = emptyMap()): List { + return fetchList(endpoint = "/creator", params = params) + } + + /** + * Retrieves details of a specific Creator by its ID. + * + * @param id The unique identifier of the Creator to retrieve. + * @return The Creator as a [Creator] object. + * @throws ServiceException If a generic error occurs during the API call. + * @throws AuthenticationException If the provided credentials are invalid. + * @throws RateLimitException If the maximum number of retries is exceeded due to rate-limiting. + */ + @Throws(ServiceException::class, AuthenticationException::class, RateLimitException::class) + fun getCreator(id: Long): Creator = fetchItem(endpoint = "/creator/$id") + + /** + * Retrieves a list of Imprints from the Metron API. + * + * @param params A map of query parameters to filter the results. + * @return A list of Imprints as [BaseResource] objects. + * @throws ServiceException If a generic error occurs during the API call. + * @throws AuthenticationException If the provided credentials are invalid. + * @throws RateLimitException If the maximum number of retries is exceeded due to rate-limiting. + */ + @Throws(ServiceException::class, AuthenticationException::class, RateLimitException::class) + fun listImprints(params: Map = emptyMap()): List { + return fetchList(endpoint = "/imprint", params = params) + } + + /** + * Retrieves details of a specific Imprint by its ID. + * + * @param id The unique identifier of the Imprint to retrieve. + * @return The Imprint as an [Imprint] object. + * @throws ServiceException If a generic error occurs during the API call. + * @throws AuthenticationException If the provided credentials are invalid. + * @throws RateLimitException If the maximum number of retries is exceeded due to rate-limiting. + */ + @Throws(ServiceException::class, AuthenticationException::class, RateLimitException::class) + fun getImprint(id: Long): Imprint = fetchItem(endpoint = "/imprint/$id") + + /** + * Retrieves a list of Issues from the Metron API. + * + * @param params A map of query parameters to filter the results. + * @return A list of Issues as [BasicIssue] objects. + * @throws ServiceException If a generic error occurs during the API call. + * @throws AuthenticationException If the provided credentials are invalid. + * @throws RateLimitException If the maximum number of retries is exceeded due to rate-limiting. + */ + @Throws(ServiceException::class, AuthenticationException::class, RateLimitException::class) + fun listIssues(params: Map = emptyMap()): List { + return fetchList(endpoint = "/issue", params = params) + } + + /** + * Retrieves details of a specific Issue by its ID. + * + * @param id The unique identifier of the Issue to retrieve. + * @return The Issue as an [Issue] object. + * @throws ServiceException If a generic error occurs during the API call. + * @throws AuthenticationException If the provided credentials are invalid. + * @throws RateLimitException If the maximum number of retries is exceeded due to rate-limiting. + */ + @Throws(ServiceException::class, AuthenticationException::class, RateLimitException::class) + fun getIssue(id: Long): Issue = fetchItem(endpoint = "/issue/$id") + + /** + * Retrieves a list of Publishers from the Metron API. + * + * @param params A map of query parameters to filter the results. + * @return A list of Publishers as [BaseResource] objects. + * @throws ServiceException If a generic error occurs during the API call. + * @throws AuthenticationException If the provided credentials are invalid. + * @throws RateLimitException If the maximum number of retries is exceeded due to rate-limiting. + */ + @Throws(ServiceException::class, AuthenticationException::class, RateLimitException::class) + fun listPublishers(params: Map = emptyMap()): List { + return fetchList(endpoint = "/publisher", params = params) + } + + /** + * Retrieves details of a specific Publisher by its ID. + * + * @param id The unique identifier of the Publisher to retrieve. + * @return The Publisher as a [Publisher] object. + * @throws ServiceException If a generic error occurs during the API call. + * @throws AuthenticationException If the provided credentials are invalid. + * @throws RateLimitException If the maximum number of retries is exceeded due to rate-limiting. + */ + @Throws(ServiceException::class, AuthenticationException::class, RateLimitException::class) + fun getPublisher(id: Long): Publisher = fetchItem(endpoint = "/publisher/$id") + + /** + * Retrieves a list of Roles from the Metron API. + * + * @param params A map of query parameters to filter the results. + * @return A list of Roles as [GenericItem] objects. + * @throws ServiceException If a generic error occurs during the API call. + * @throws AuthenticationException If the provided credentials are invalid. + * @throws RateLimitException If the maximum number of retries is exceeded due to rate-limiting. + */ + @Throws(ServiceException::class, AuthenticationException::class, RateLimitException::class) + fun listRoles(params: Map = emptyMap()): List { + return fetchList(endpoint = "/role", params = params) + } + + /** + * Retrieves a list of Series from the Metron API. + * + * @param params A map of query parameters to filter the results. + * @return A list of Series as [BasicSeries] objects. + * @throws ServiceException If a generic error occurs during the API call. + * @throws AuthenticationException If the provided credentials are invalid. + * @throws RateLimitException If the maximum number of retries is exceeded due to rate-limiting. + */ + @Throws(ServiceException::class, AuthenticationException::class, RateLimitException::class) + fun listSeries(params: Map = emptyMap()): List { + return fetchList(endpoint = "/series", params = params) + } + + /** + * Retrieves details of a specific Series by its ID. + * + * @param id The unique identifier of the Series to retrieve. + * @return The Series as a [Series] object. + * @throws ServiceException If a generic error occurs during the API call. + * @throws AuthenticationException If the provided credentials are invalid. + * @throws RateLimitException If the maximum number of retries is exceeded due to rate-limiting. + */ + @Throws(ServiceException::class, AuthenticationException::class, RateLimitException::class) + fun getSeries(id: Long): Series = fetchItem(endpoint = "/series/$id") + + /** + * Retrieves a list of SeriesTypes from the Metron API. + * + * @param params A map of query parameters to filter the results. + * @return A list of SeriesTypes as [GenericItem] objects. + * @throws ServiceException If a generic error occurs during the API call. + * @throws AuthenticationException If the provided credentials are invalid. + * @throws RateLimitException If the maximum number of retries is exceeded due to rate-limiting. + */ + @Throws(ServiceException::class, AuthenticationException::class, RateLimitException::class) + fun listSeriesTypes(params: Map = emptyMap()): List { + return fetchList(endpoint = "/series_type", params = params) + } + + /** + * Retrieves a list of Teams from the Metron API. + * + * @param params A map of query parameters to filter the results. + * @return A list of Teams as [BaseResource] objects. + * @throws ServiceException If a generic error occurs during the API call. + * @throws AuthenticationException If the provided credentials are invalid. + * @throws RateLimitException If the maximum number of retries is exceeded due to rate-limiting. + */ + @Throws(ServiceException::class, AuthenticationException::class, RateLimitException::class) + fun listTeams(params: Map = emptyMap()): List { + return fetchList(endpoint = "/team", params = params) + } + + /** + * Retrieves details of a specific Team by its ID. + * + * @param id The unique identifier of the Team to retrieve. + * @return The Team as a [Team] object. + * @throws ServiceException If a generic error occurs during the API call. + * @throws AuthenticationException If the provided credentials are invalid. + * @throws RateLimitException If the maximum number of retries is exceeded due to rate-limiting. + */ + @Throws(ServiceException::class, AuthenticationException::class, RateLimitException::class) + fun getTeam(id: Long): Team = fetchItem(endpoint = "/team/$id") + + /** + * Retrieves a list of Universes from the Metron API. + * + * @param params A map of query parameters to filter the results. + * @return A list of Universes as [BaseResource] objects. + * @throws ServiceException If a generic error occurs during the API call. + * @throws AuthenticationException If the provided credentials are invalid. + * @throws RateLimitException If the maximum number of retries is exceeded due to rate-limiting. + */ + @Throws(ServiceException::class, AuthenticationException::class, RateLimitException::class) + fun listUniverses(params: Map = emptyMap()): List { + return fetchList(endpoint = "/universe", params = params) + } + + /** + * Retrieves details of a specific Universe by its ID. + * + * @param id The unique identifier of the Universe to retrieve. + * @return The Universe as an [Universe] object. + * @throws ServiceException If a generic error occurs during the API call. + * @throws AuthenticationException If the provided credentials are invalid. + * @throws RateLimitException If the maximum number of retries is exceeded due to rate-limiting. + */ + @Throws(ServiceException::class, AuthenticationException::class, RateLimitException::class) + fun getUniverse(id: Long): Universe = fetchItem(endpoint = "/universe/$id") + + companion object { + @JvmStatic private val LOGGER = KotlinLogging.logger {} + + private const val BASE_API = "https://metron.cloud/api" + + @OptIn(ExperimentalSerializationApi::class) + private val JSON: Json = Json { + prettyPrint = true + encodeDefaults = true + namingStrategy = JsonNamingStrategy.SnakeCase } + } } diff --git a/src/main/kotlin/github/buriedincode/kraken/SQLiteCache.kt b/src/main/kotlin/github/buriedincode/kraken/SQLiteCache.kt index dbf0444..af8e4a5 100644 --- a/src/main/kotlin/github/buriedincode/kraken/SQLiteCache.kt +++ b/src/main/kotlin/github/buriedincode/kraken/SQLiteCache.kt @@ -8,113 +8,109 @@ import java.time.LocalDate /** * A simple SQLite-based caching mechanism for storing and retrieving HTTP query results. * - * The `SQLiteCache` class provides methods to persist query results, retrieve them later, and automatically clean up expired entries based on a configurable expiry period. + * The `SQLiteCache` class provides methods to persist query results, retrieve them later, and automatically clean up + * expired entries based on a configurable expiry period. * * @property path The file path to the SQLite database file. * @property expiry The number of days before cached entries expire. If `null`, entries will not expire. * @constructor Initializes the SQLite cache, creating the necessary table and performing cleanup for expired entries. */ data class SQLiteCache(val path: Path, val expiry: Int? = null) { - private val databaseUrl: String = "jdbc:sqlite:$path" + private val databaseUrl: String = "jdbc:sqlite:$path" - init { - this.createTable() - this.cleanup() - } + init { + this.createTable() + this.cleanup() + } - /** - * Creates the `queries` table in the SQLite database if it does not already exist. - */ - private fun createTable() { - val query = "CREATE TABLE IF NOT EXISTS queries (url, response, query_date);" - DriverManager.getConnection(this.databaseUrl).use { - it.createStatement().use { - it.execute(query) - } - } - } + /** Creates the `queries` table in the SQLite database if it does not already exist. */ + private fun createTable() { + val query = "CREATE TABLE IF NOT EXISTS queries (url, response, query_date);" + DriverManager.getConnection(this.databaseUrl).use { it.createStatement().use { it.execute(query) } } + } - /** - * Selects a cached response for a given URL. - * - * If an expiry is set, only entries that have not expired will be retrieved. - * - * @param url The URL whose cached response is to be retrieved. - * @return The cached response as a string, or `null` if no valid entry exists. - */ - fun select(url: String): String? { - val query = if (this.expiry == null) { - "SELECT * FROM queries WHERE url = ?;" - } else { - "SELECT * FROM queries WHERE url = ? and query_date > ?;" + /** + * Selects a cached response for a given URL. + * + * If an expiry is set, only entries that have not expired will be retrieved. + * + * @param url The URL whose cached response is to be retrieved. + * @return The cached response as a string, or `null` if no valid entry exists. + */ + fun select(url: String): String? { + val query = + if (this.expiry == null) { + "SELECT * FROM queries WHERE url = ?;" + } else { + "SELECT * FROM queries WHERE url = ? and query_date > ?;" + } + DriverManager.getConnection(this.databaseUrl).use { + it.prepareStatement(query).use { + it.setString(1, url) + if (this.expiry != null) { + it.setDate(2, Date.valueOf(LocalDate.now().minusDays(this.expiry.toLong()))) } - DriverManager.getConnection(this.databaseUrl).use { - it.prepareStatement(query).use { - it.setString(1, url) - if (this.expiry != null) { - it.setDate(2, Date.valueOf(LocalDate.now().minusDays(this.expiry.toLong()))) - } - it.executeQuery().use { - return it.getString("response") - } - } + it.executeQuery().use { + return it.getString("response") } + } } + } - /** - * Inserts a new cached response for a given URL. - * - * If an entry for the URL already exists, the method does nothing. - * - * @param url The URL whose response is to be cached. - * @param response The response to cache as a string. - */ - fun insert(url: String, response: String) { - if (this.select(url = url) != null) { - return - } - val query = "INSERT INTO queries (url, response, query_date) VALUES (?, ?, ?);" - DriverManager.getConnection(this.databaseUrl).use { - it.prepareStatement(query).use { - it.setString(1, url) - it.setString(2, response) - it.setDate(3, Date.valueOf(LocalDate.now())) - it.executeUpdate() - } - } + /** + * Inserts a new cached response for a given URL. + * + * If an entry for the URL already exists, the method does nothing. + * + * @param url The URL whose response is to be cached. + * @param response The response to cache as a string. + */ + fun insert(url: String, response: String) { + if (this.select(url = url) != null) { + return } + val query = "INSERT INTO queries (url, response, query_date) VALUES (?, ?, ?);" + DriverManager.getConnection(this.databaseUrl).use { + it.prepareStatement(query).use { + it.setString(1, url) + it.setString(2, response) + it.setDate(3, Date.valueOf(LocalDate.now())) + it.executeUpdate() + } + } + } - /** - * Deletes a cached response for a given URL. - * - * @param url The URL whose cached response is to be deleted. - */ - fun delete(url: String) { - val query = "DELETE FROM queries WHERE url = ?;" - DriverManager.getConnection(this.databaseUrl).use { - it.prepareStatement(query).use { - it.setString(1, url) - it.executeUpdate() - } - } + /** + * Deletes a cached response for a given URL. + * + * @param url The URL whose cached response is to be deleted. + */ + fun delete(url: String) { + val query = "DELETE FROM queries WHERE url = ?;" + DriverManager.getConnection(this.databaseUrl).use { + it.prepareStatement(query).use { + it.setString(1, url) + it.executeUpdate() + } } + } - /** - * Cleans up expired entries in the cache. - * - * If an expiry is set, this method removes all entries with a `query_date` older than the expiry period. - */ - fun cleanup() { - if (this.expiry == null) { - return - } - val query = "DELETE FROM queries WHERE query_date < ?;" - val expiryDate = LocalDate.now().minusDays(this.expiry.toLong()) - DriverManager.getConnection(this.databaseUrl).use { - it.prepareStatement(query).use { - it.setDate(1, Date.valueOf(expiryDate)) - it.executeUpdate() - } - } + /** + * Cleans up expired entries in the cache. + * + * If an expiry is set, this method removes all entries with a `query_date` older than the expiry period. + */ + fun cleanup() { + if (this.expiry == null) { + return + } + val query = "DELETE FROM queries WHERE query_date < ?;" + val expiryDate = LocalDate.now().minusDays(this.expiry.toLong()) + DriverManager.getConnection(this.databaseUrl).use { + it.prepareStatement(query).use { + it.setDate(1, Date.valueOf(expiryDate)) + it.executeUpdate() + } } + } } diff --git a/src/main/kotlin/github/buriedincode/kraken/Utils.kt b/src/main/kotlin/github/buriedincode/kraken/Utils.kt index a73ed28..95e0568 100644 --- a/src/main/kotlin/github/buriedincode/kraken/Utils.kt +++ b/src/main/kotlin/github/buriedincode/kraken/Utils.kt @@ -6,24 +6,23 @@ import io.github.oshai.kotlinlogging.Level /** * Logs a message at a specified logging level using the [KLogger]. * - * This utility function provides a consistent way to log messages across different levels (e.g., TRACE, DEBUG, INFO, WARN, ERROR) by delegating to the corresponding logging method of the [KLogger]. + * This utility function provides a consistent way to log messages across different levels (e.g., TRACE, DEBUG, INFO, + * WARN, ERROR) by delegating to the corresponding logging method of the [KLogger]. * - * @receiver [KLogger] The logger instance to log the message. * @param level The logging level at which the message should be logged. * @param message A lambda function that produces the log message. + * @receiver [KLogger] The logger instance to log the message. */ internal fun KLogger.log(level: Level, message: () -> Any?) { - when (level) { - Level.TRACE -> this.trace(message) - Level.DEBUG -> this.debug(message) - Level.INFO -> this.info(message) - Level.WARN -> this.warn(message) - Level.ERROR -> this.error(message) - else -> return - } + when (level) { + Level.TRACE -> this.trace(message) + Level.DEBUG -> this.debug(message) + Level.INFO -> this.info(message) + Level.WARN -> this.warn(message) + Level.ERROR -> this.error(message) + else -> return + } } -/** - * The version of the Kraken library. - */ -internal const val VERSION = "0.3.1" +/** The version of the Kraken library. */ +internal const val VERSION = "0.4.0" diff --git a/src/main/kotlin/github/buriedincode/kraken/schemas/Arc.kt b/src/main/kotlin/github/buriedincode/kraken/schemas/Arc.kt index 82c4042..583877d 100644 --- a/src/main/kotlin/github/buriedincode/kraken/schemas/Arc.kt +++ b/src/main/kotlin/github/buriedincode/kraken/schemas/Arc.kt @@ -1,7 +1,8 @@ package github.buriedincode.kraken.schemas import github.buriedincode.kraken.serializers.NullableStringSerializer -import kotlinx.datetime.Instant +import kotlin.time.ExperimentalTime +import kotlin.time.Instant import kotlinx.serialization.ExperimentalSerializationApi import kotlinx.serialization.Serializable import kotlinx.serialization.json.JsonNames @@ -18,20 +19,15 @@ import kotlinx.serialization.json.JsonNames * @property name The name of the resource. * @property resourceUrl The URL of the arc resource. */ -@OptIn(ExperimentalSerializationApi::class) +@OptIn(ExperimentalSerializationApi::class, ExperimentalTime::class) @Serializable data class Arc( - @JsonNames("cv_id") - val comicvineId: Long? = null, - @Serializable(with = NullableStringSerializer::class) - @JsonNames("desc") - val description: String? = null, - @JsonNames("gcd_id") - val grandComicsDatabaseId: Long? = null, - val id: Long, - @Serializable(with = NullableStringSerializer::class) - val image: String? = null, - val modified: Instant, - val name: String, - val resourceUrl: String, + @JsonNames("cv_id") val comicvineId: Long? = null, + @Serializable(with = NullableStringSerializer::class) @JsonNames("desc") val description: String? = null, + @JsonNames("gcd_id") val grandComicsDatabaseId: Long? = null, + val id: Long, + @Serializable(with = NullableStringSerializer::class) val image: String? = null, + val modified: Instant, + val name: String, + val resourceUrl: String, ) diff --git a/src/main/kotlin/github/buriedincode/kraken/schemas/Character.kt b/src/main/kotlin/github/buriedincode/kraken/schemas/Character.kt index c3c0ba0..06636f8 100644 --- a/src/main/kotlin/github/buriedincode/kraken/schemas/Character.kt +++ b/src/main/kotlin/github/buriedincode/kraken/schemas/Character.kt @@ -2,7 +2,8 @@ package github.buriedincode.kraken.schemas import github.buriedincode.kraken.serializers.EmptyListSerializer import github.buriedincode.kraken.serializers.NullableStringSerializer -import kotlinx.datetime.Instant +import kotlin.time.ExperimentalTime +import kotlin.time.Instant import kotlinx.serialization.ExperimentalSerializationApi import kotlinx.serialization.Serializable import kotlinx.serialization.json.JsonNames @@ -23,25 +24,19 @@ import kotlinx.serialization.json.JsonNames * @property teams The teams the character belongs to. * @property universes The universes the character is associated with. */ -@OptIn(ExperimentalSerializationApi::class) +@OptIn(ExperimentalSerializationApi::class, ExperimentalTime::class) @Serializable data class Character( - @Serializable(with = EmptyListSerializer::class) - val alias: List = emptyList(), - @JsonNames("cv_id") - val comicvineId: Long? = null, - val creators: List = emptyList(), - @Serializable(with = NullableStringSerializer::class) - @JsonNames("desc") - val description: String? = null, - @JsonNames("gcd_id") - val grandComicsDatabaseId: Long? = null, - val id: Long, - @Serializable(with = NullableStringSerializer::class) - val image: String? = null, - val modified: Instant, - val name: String, - val resourceUrl: String, - val teams: List = emptyList(), - val universes: List = emptyList(), + @Serializable(with = EmptyListSerializer::class) val alias: List = emptyList(), + @JsonNames("cv_id") val comicvineId: Long? = null, + val creators: List = emptyList(), + @Serializable(with = NullableStringSerializer::class) @JsonNames("desc") val description: String? = null, + @JsonNames("gcd_id") val grandComicsDatabaseId: Long? = null, + val id: Long, + @Serializable(with = NullableStringSerializer::class) val image: String? = null, + val modified: Instant, + val name: String, + val resourceUrl: String, + val teams: List = emptyList(), + val universes: List = emptyList(), ) diff --git a/src/main/kotlin/github/buriedincode/kraken/schemas/Common.kt b/src/main/kotlin/github/buriedincode/kraken/schemas/Common.kt index 0dd508e..9863b18 100644 --- a/src/main/kotlin/github/buriedincode/kraken/schemas/Common.kt +++ b/src/main/kotlin/github/buriedincode/kraken/schemas/Common.kt @@ -1,7 +1,8 @@ package github.buriedincode.kraken.schemas import github.buriedincode.kraken.serializers.NullableStringSerializer -import kotlinx.datetime.Instant +import kotlin.time.ExperimentalTime +import kotlin.time.Instant import kotlinx.serialization.Serializable /** @@ -12,16 +13,13 @@ import kotlinx.serialization.Serializable * @property next The URL for the next page of results, if available. `null` if there are no more pages. * @property previous The URL for the previous page of results, if available. `null` if on the first page. * @property results A list of items of type `T` returned by the API. - * */ @Serializable data class ListResponse( - val count: Int, - @Serializable(with = NullableStringSerializer::class) - val next: String? = null, - @Serializable(with = NullableStringSerializer::class) - val previous: String? = null, - val results: List = listOf(), + val count: Int, + @Serializable(with = NullableStringSerializer::class) val next: String? = null, + @Serializable(with = NullableStringSerializer::class) val previous: String? = null, + val results: List = listOf(), ) /** @@ -30,11 +28,7 @@ data class ListResponse( * @property id The unique identifier of the generic item. * @property name The name fo the generic item. */ -@Serializable -data class GenericItem( - val id: Long, - val name: String, -) +@Serializable data class GenericItem(val id: Long, val name: String) /** * A data model representing a base resource. @@ -43,9 +37,6 @@ data class GenericItem( * @property modified The date and time when the base resource was last modified. * @property name The name of the base resource. */ +@OptIn(ExperimentalTime::class) @Serializable -data class BaseResource( - val id: Long, - val modified: Instant, - val name: String, -) +data class BaseResource(val id: Long, val modified: Instant, val name: String) diff --git a/src/main/kotlin/github/buriedincode/kraken/schemas/Creator.kt b/src/main/kotlin/github/buriedincode/kraken/schemas/Creator.kt index 06125b8..9289a69 100644 --- a/src/main/kotlin/github/buriedincode/kraken/schemas/Creator.kt +++ b/src/main/kotlin/github/buriedincode/kraken/schemas/Creator.kt @@ -1,7 +1,8 @@ package github.buriedincode.kraken.schemas import github.buriedincode.kraken.serializers.NullableStringSerializer -import kotlinx.datetime.Instant +import kotlin.time.ExperimentalTime +import kotlin.time.Instant import kotlinx.datetime.LocalDate import kotlinx.serialization.ExperimentalSerializationApi import kotlinx.serialization.Serializable @@ -22,23 +23,18 @@ import kotlinx.serialization.json.JsonNames * @property name The name of the resource. * @property resourceUrl The URL of the creator resource. */ -@OptIn(ExperimentalSerializationApi::class) +@OptIn(ExperimentalSerializationApi::class, ExperimentalTime::class) @Serializable data class Creator( - val alias: List = emptyList(), - val birth: LocalDate? = null, - @JsonNames("cv_id") - val comicvineId: Long? = null, - val death: LocalDate? = null, - @Serializable(with = NullableStringSerializer::class) - @JsonNames("desc") - val description: String? = null, - @JsonNames("gcd_id") - val grandComicsDatabaseId: Long? = null, - val id: Long, - @Serializable(with = NullableStringSerializer::class) - val image: String? = null, - val modified: Instant, - val name: String, - val resourceUrl: String, + val alias: List = emptyList(), + val birth: LocalDate? = null, + @JsonNames("cv_id") val comicvineId: Long? = null, + val death: LocalDate? = null, + @Serializable(with = NullableStringSerializer::class) @JsonNames("desc") val description: String? = null, + @JsonNames("gcd_id") val grandComicsDatabaseId: Long? = null, + val id: Long, + @Serializable(with = NullableStringSerializer::class) val image: String? = null, + val modified: Instant, + val name: String, + val resourceUrl: String, ) diff --git a/src/main/kotlin/github/buriedincode/kraken/schemas/Imprint.kt b/src/main/kotlin/github/buriedincode/kraken/schemas/Imprint.kt index 56b6a8b..0a55645 100644 --- a/src/main/kotlin/github/buriedincode/kraken/schemas/Imprint.kt +++ b/src/main/kotlin/github/buriedincode/kraken/schemas/Imprint.kt @@ -1,7 +1,8 @@ package github.buriedincode.kraken.schemas import github.buriedincode.kraken.serializers.NullableStringSerializer -import kotlinx.datetime.Instant +import kotlin.time.ExperimentalTime +import kotlin.time.Instant import kotlinx.serialization.ExperimentalSerializationApi import kotlinx.serialization.Serializable import kotlinx.serialization.json.JsonNames @@ -20,22 +21,17 @@ import kotlinx.serialization.json.JsonNames * @property publisher The generic item representing the publisher. * @property resourceUrl The URL of the publisher resource. */ -@OptIn(ExperimentalSerializationApi::class) +@OptIn(ExperimentalSerializationApi::class, ExperimentalTime::class) @Serializable data class Imprint( - @JsonNames("cv_id") - val comicvineId: Long? = null, - @Serializable(with = NullableStringSerializer::class) - @JsonNames("desc") - val description: String? = null, - val founded: Int? = null, - @JsonNames("gcd_id") - val grandComicsDatabaseId: Long? = null, - val id: Long, - @Serializable(with = NullableStringSerializer::class) - val image: String? = null, - val modified: Instant, - val name: String, - val publisher: GenericItem, - val resourceUrl: String, + @JsonNames("cv_id") val comicvineId: Long? = null, + @Serializable(with = NullableStringSerializer::class) @JsonNames("desc") val description: String? = null, + val founded: Int? = null, + @JsonNames("gcd_id") val grandComicsDatabaseId: Long? = null, + val id: Long, + @Serializable(with = NullableStringSerializer::class) val image: String? = null, + val modified: Instant, + val name: String, + val publisher: GenericItem, + val resourceUrl: String, ) diff --git a/src/main/kotlin/github/buriedincode/kraken/schemas/Issue.kt b/src/main/kotlin/github/buriedincode/kraken/schemas/Issue.kt index d3d5841..312ec74 100644 --- a/src/main/kotlin/github/buriedincode/kraken/schemas/Issue.kt +++ b/src/main/kotlin/github/buriedincode/kraken/schemas/Issue.kt @@ -1,7 +1,8 @@ package github.buriedincode.kraken.schemas import github.buriedincode.kraken.serializers.NullableStringSerializer -import kotlinx.datetime.Instant +import kotlin.time.ExperimentalTime +import kotlin.time.Instant import kotlinx.datetime.LocalDate import kotlinx.serialization.ExperimentalSerializationApi import kotlinx.serialization.Serializable @@ -20,35 +21,27 @@ import kotlinx.serialization.json.JsonNames * @property storeDate The store date of the issue. * @property title The name of the issue. */ -@OptIn(ExperimentalSerializationApi::class) +@OptIn(ExperimentalSerializationApi::class, ExperimentalTime::class) @Serializable data class BasicIssue( - val coverDate: LocalDate, - @Serializable(with = NullableStringSerializer::class) - val coverHash: String? = null, - val id: Long, - @Serializable(with = NullableStringSerializer::class) - val image: String? = null, - val modified: Instant, - val number: String, - val series: Series, - val storeDate: LocalDate? = null, - @JsonNames("issue") - val title: String, + val coverDate: LocalDate, + @Serializable(with = NullableStringSerializer::class) val coverHash: String? = null, + val id: Long, + @Serializable(with = NullableStringSerializer::class) val image: String? = null, + val modified: Instant, + val number: String, + val series: Series, + val storeDate: LocalDate? = null, + @JsonNames("issue") val title: String, ) { - /** - * A class representing a basic series with name, volume and year began. - * - * @property name The name of the series. - * @property volume The volume of the series. - * @property yearBegan The year the series began. - */ - @Serializable - data class Series( - val name: String, - val volume: Int, - val yearBegan: Int, - ) + /** + * A class representing a basic series with name, volume and year began. + * + * @property name The name of the series. + * @property volume The volume of the series. + * @property yearBegan The year the series began. + */ + @Serializable data class Series(val name: String, val volume: Int, val yearBegan: Int) } /** @@ -62,6 +55,7 @@ data class BasicIssue( * @property coverHash The hash value of the issue cover. * @property credits The credits for the issue. * @property description The description of the issue. + * @property focDate The final order cutoff date of the issue. * @property grandComicsDatabaseId The Grand Comics Database ID of the issue. * @property id The unique identifier of the issue. * @property image The image URL of the issue. @@ -85,120 +79,94 @@ data class BasicIssue( * @property upc The Universal Product Code (UPC) of the issue. * @property variants The variants of the issue. */ -@OptIn(ExperimentalSerializationApi::class) +@OptIn(ExperimentalSerializationApi::class, ExperimentalTime::class) @Serializable data class Issue( - @JsonNames("alt_number") - @Serializable(with = NullableStringSerializer::class) - val alternativeNumber: String? = null, - val arcs: List = emptyList(), - val characters: List = emptyList(), - @JsonNames("cv_id") - val comicvineId: Long? = null, - val coverDate: LocalDate, - @Serializable(with = NullableStringSerializer::class) - val coverHash: String? = null, - val credits: List = emptyList(), - @JsonNames("desc") - @Serializable(with = NullableStringSerializer::class) - val description: String? = null, - @JsonNames("gcd_id") - val grandComicsDatabaseId: Long? = null, - val id: Long, - @Serializable(with = NullableStringSerializer::class) - val image: String? = null, - val imprint: GenericItem? = null, - @Serializable(with = NullableStringSerializer::class) - val isbn: String? = null, - val modified: Instant, - val number: String, - @JsonNames("page") - val pageCount: Int? = null, - val price: Double? = null, - val publisher: GenericItem, - val rating: GenericItem, - val reprints: List = emptyList(), - val resourceUrl: String, - val series: Series, - @Serializable(with = NullableStringSerializer::class) - val sku: String? = null, - val storeDate: LocalDate? = null, - @JsonNames("name") - val stories: List = emptyList(), - val teams: List = emptyList(), - @Serializable(with = NullableStringSerializer::class) - val title: String? = null, - val universes: List = emptyList(), - @Serializable(with = NullableStringSerializer::class) - val upc: String? = null, - val variants: List = emptyList(), + @JsonNames("alt_number") @Serializable(with = NullableStringSerializer::class) val alternativeNumber: String? = null, + val arcs: List = emptyList(), + val characters: List = emptyList(), + @JsonNames("cv_id") val comicvineId: Long? = null, + val coverDate: LocalDate, + @Serializable(with = NullableStringSerializer::class) val coverHash: String? = null, + val credits: List = emptyList(), + @JsonNames("desc") @Serializable(with = NullableStringSerializer::class) val description: String? = null, + val focDate: LocalDate? = null, + @JsonNames("gcd_id") val grandComicsDatabaseId: Long? = null, + val id: Long, + @Serializable(with = NullableStringSerializer::class) val image: String? = null, + val imprint: GenericItem? = null, + @Serializable(with = NullableStringSerializer::class) val isbn: String? = null, + val modified: Instant, + val number: String, + @JsonNames("page") val pageCount: Int? = null, + val price: Double? = null, + val publisher: GenericItem, + val rating: GenericItem, + val reprints: List = emptyList(), + val resourceUrl: String, + val series: Series, + @Serializable(with = NullableStringSerializer::class) val sku: String? = null, + val storeDate: LocalDate? = null, + @JsonNames("name") val stories: List = emptyList(), + val teams: List = emptyList(), + @Serializable(with = NullableStringSerializer::class) val title: String? = null, + val universes: List = emptyList(), + @Serializable(with = NullableStringSerializer::class) val upc: String? = null, + val variants: List = emptyList(), ) { - /** - * A class representing a credit with ID, creator and roles. - * - * @property creator The creator associated with the credit. - * @property id The ID of the credit. - * @property roles The list of roles the creator has in this credit. - */ - @Serializable - data class Credit( - val creator: String, - val id: Long, - @JsonNames("role") - val roles: List = emptyList(), - ) + /** + * A class representing a credit with ID, creator and roles. + * + * @property creator The creator associated with the credit. + * @property id The ID of the credit. + * @property roles The list of roles the creator has in this credit. + */ + @Serializable + data class Credit(val creator: String, val id: Long, @JsonNames("role") val roles: List = emptyList()) - /** - * A data model representing a reprint. - * - * @property id The unique identifier of the reprint. - * @property issue The issue being reprinted. - */ - @OptIn(ExperimentalSerializationApi::class) - @Serializable - data class Reprint( - val id: Long, - val issue: String, - ) + /** + * A data model representing a reprint. + * + * @property id The unique identifier of the reprint. + * @property issue The issue being reprinted. + */ + @OptIn(ExperimentalSerializationApi::class) @Serializable data class Reprint(val id: Long, val issue: String) - /** - * A data model representing an issue series. - * - * @property genres The genres associated with the series. - * @property id The unique identifier of the series. - * @property name The name of the series. - * @property seriesType The type of series. - * @property sortName The name used for sorting the series. - * @property volume The volume number of the series. - * @property yearBegan The year the series began. - */ - @Serializable - data class Series( - val genres: List = emptyList(), - val id: Long, - val name: String, - val seriesType: GenericItem, - val sortName: String, - val volume: Int, - val yearBegan: Int, - ) + /** + * A data model representing an issue series. + * + * @property genres The genres associated with the series. + * @property id The unique identifier of the series. + * @property name The name of the series. + * @property seriesType The type of series. + * @property sortName The name used for sorting the series. + * @property volume The volume number of the series. + * @property yearBegan The year the series began. + */ + @Serializable + data class Series( + val genres: List = emptyList(), + val id: Long, + val name: String, + val seriesType: GenericItem, + val sortName: String, + val volume: Int, + val yearBegan: Int, + ) - /** - * A data model representing a variant cover. - * - * @property image The image URL of the variant. - * @property name The name of the variant. - * @property sku The Stock Keeping Unit (SKU) of the variant. - * @property upc The Universal Product Code (UPC) of the variant. - */ - @Serializable - data class Variant( - val image: String, - @Serializable(with = NullableStringSerializer::class) - val name: String? = null, - @Serializable(with = NullableStringSerializer::class) - val sku: String? = null, - @Serializable(with = NullableStringSerializer::class) - val upc: String? = null, - ) + /** + * A data model representing a variant cover. + * + * @property image The image URL of the variant. + * @property name The name of the variant. + * @property sku The Stock Keeping Unit (SKU) of the variant. + * @property upc The Universal Product Code (UPC) of the variant. + */ + @Serializable + data class Variant( + val image: String, + @Serializable(with = NullableStringSerializer::class) val name: String? = null, + @Serializable(with = NullableStringSerializer::class) val sku: String? = null, + @Serializable(with = NullableStringSerializer::class) val upc: String? = null, + ) } diff --git a/src/main/kotlin/github/buriedincode/kraken/schemas/Publisher.kt b/src/main/kotlin/github/buriedincode/kraken/schemas/Publisher.kt index 0c35438..462aac2 100644 --- a/src/main/kotlin/github/buriedincode/kraken/schemas/Publisher.kt +++ b/src/main/kotlin/github/buriedincode/kraken/schemas/Publisher.kt @@ -1,7 +1,8 @@ package github.buriedincode.kraken.schemas import github.buriedincode.kraken.serializers.NullableStringSerializer -import kotlinx.datetime.Instant +import kotlin.time.ExperimentalTime +import kotlin.time.Instant import kotlinx.serialization.ExperimentalSerializationApi import kotlinx.serialization.Serializable import kotlinx.serialization.json.JsonNames @@ -20,22 +21,17 @@ import kotlinx.serialization.json.JsonNames * @property name The name of the resource. * @property resourceUrl The URL of the publisher. */ -@OptIn(ExperimentalSerializationApi::class) +@OptIn(ExperimentalSerializationApi::class, ExperimentalTime::class) @Serializable data class Publisher( - @JsonNames("cv_id") - val comicvineId: Long? = null, - val country: String, - @Serializable(with = NullableStringSerializer::class) - @JsonNames("desc") - val description: String? = null, - val founded: Int? = null, - @JsonNames("gcd_id") - val grandComicsDatabaseId: Long? = null, - val id: Long, - @Serializable(with = NullableStringSerializer::class) - val image: String? = null, - val modified: Instant, - val name: String, - val resourceUrl: String, + @JsonNames("cv_id") val comicvineId: Long? = null, + val country: String, + @Serializable(with = NullableStringSerializer::class) @JsonNames("desc") val description: String? = null, + val founded: Int? = null, + @JsonNames("gcd_id") val grandComicsDatabaseId: Long? = null, + val id: Long, + @Serializable(with = NullableStringSerializer::class) val image: String? = null, + val modified: Instant, + val name: String, + val resourceUrl: String, ) diff --git a/src/main/kotlin/github/buriedincode/kraken/schemas/Series.kt b/src/main/kotlin/github/buriedincode/kraken/schemas/Series.kt index 6a01723..8550c71 100644 --- a/src/main/kotlin/github/buriedincode/kraken/schemas/Series.kt +++ b/src/main/kotlin/github/buriedincode/kraken/schemas/Series.kt @@ -1,7 +1,8 @@ package github.buriedincode.kraken.schemas import github.buriedincode.kraken.serializers.NullableStringSerializer -import kotlinx.datetime.Instant +import kotlin.time.ExperimentalTime +import kotlin.time.Instant import kotlinx.serialization.ExperimentalSerializationApi import kotlinx.serialization.Serializable import kotlinx.serialization.json.JsonNames @@ -16,16 +17,15 @@ import kotlinx.serialization.json.JsonNames * @property volume The volume number of the series. * @property yearBegan The year the series began. */ -@OptIn(ExperimentalSerializationApi::class) +@OptIn(ExperimentalSerializationApi::class, ExperimentalTime::class) @Serializable data class BasicSeries( - val id: Long, - val issueCount: Int, - val modified: Instant, - @JsonNames("series") - val name: String, - val volume: Int, - val yearBegan: Int, + val id: Long, + val issueCount: Int, + val modified: Instant, + @JsonNames("series") val name: String, + val volume: Int, + val yearBegan: Int, ) /** @@ -50,43 +50,35 @@ data class BasicSeries( * @property yearBegan The year the series began. * @property yearEnd The year the series ended. */ -@OptIn(ExperimentalSerializationApi::class) +@OptIn(ExperimentalSerializationApi::class, ExperimentalTime::class) @Serializable data class Series( - val associated: List = emptyList(), - @JsonNames("cv_id") - val comicvineId: Long? = null, - @Serializable(with = NullableStringSerializer::class) - @JsonNames("desc") - val description: String? = null, - val genres: List = emptyList(), - @JsonNames("gcd_id") - val grandComicsDatabaseId: Long? = null, - val id: Long, - val imprint: GenericItem? = null, - val issueCount: Int, - val modified: Instant, - val name: String, - val publisher: GenericItem, - val resourceUrl: String, - val seriesType: GenericItem, - val status: String, - val sortName: String, - val volume: Int, - val yearBegan: Int, - val yearEnd: Int? = null, + val associated: List = emptyList(), + @JsonNames("cv_id") val comicvineId: Long? = null, + @Serializable(with = NullableStringSerializer::class) @JsonNames("desc") val description: String? = null, + val genres: List = emptyList(), + @JsonNames("gcd_id") val grandComicsDatabaseId: Long? = null, + val id: Long, + val imprint: GenericItem? = null, + val issueCount: Int, + val modified: Instant, + val name: String, + val publisher: GenericItem, + val resourceUrl: String, + val seriesType: GenericItem, + val status: String, + val sortName: String, + val volume: Int, + val yearBegan: Int, + val yearEnd: Int? = null, ) { - /** - * A data model representing an associated series. - * - * @property id The unique identifier of the associated series. - * @property name The name of the associated series. - */ - @OptIn(ExperimentalSerializationApi::class) - @Serializable - data class Associated( - val id: Long, - @JsonNames("series") - val name: String, - ) + /** + * A data model representing an associated series. + * + * @property id The unique identifier of the associated series. + * @property name The name of the associated series. + */ + @OptIn(ExperimentalSerializationApi::class) + @Serializable + data class Associated(val id: Long, @JsonNames("series") val name: String) } diff --git a/src/main/kotlin/github/buriedincode/kraken/schemas/Team.kt b/src/main/kotlin/github/buriedincode/kraken/schemas/Team.kt index 6afcc47..f6d364f 100644 --- a/src/main/kotlin/github/buriedincode/kraken/schemas/Team.kt +++ b/src/main/kotlin/github/buriedincode/kraken/schemas/Team.kt @@ -1,7 +1,8 @@ package github.buriedincode.kraken.schemas import github.buriedincode.kraken.serializers.NullableStringSerializer -import kotlinx.datetime.Instant +import kotlin.time.ExperimentalTime +import kotlin.time.Instant import kotlinx.serialization.ExperimentalSerializationApi import kotlinx.serialization.Serializable import kotlinx.serialization.json.JsonNames @@ -20,22 +21,17 @@ import kotlinx.serialization.json.JsonNames * @property resourceUrl The URL of the team. * @property universes The universes the team is associated with. */ -@OptIn(ExperimentalSerializationApi::class) +@OptIn(ExperimentalSerializationApi::class, ExperimentalTime::class) @Serializable data class Team( - @JsonNames("cv_id") - val comicvineId: Long? = null, - val creators: List = emptyList(), - @Serializable(with = NullableStringSerializer::class) - @JsonNames("desc") - val description: String? = null, - @JsonNames("gcd_id") - val grandComicsDatabaseId: Long? = null, - val id: Long, - @Serializable(with = NullableStringSerializer::class) - val image: String? = null, - val modified: Instant, - val name: String, - val resourceUrl: String, - val universes: List = emptyList(), + @JsonNames("cv_id") val comicvineId: Long? = null, + val creators: List = emptyList(), + @Serializable(with = NullableStringSerializer::class) @JsonNames("desc") val description: String? = null, + @JsonNames("gcd_id") val grandComicsDatabaseId: Long? = null, + val id: Long, + @Serializable(with = NullableStringSerializer::class) val image: String? = null, + val modified: Instant, + val name: String, + val resourceUrl: String, + val universes: List = emptyList(), ) diff --git a/src/main/kotlin/github/buriedincode/kraken/schemas/Universe.kt b/src/main/kotlin/github/buriedincode/kraken/schemas/Universe.kt index e5a45fc..41ae121 100644 --- a/src/main/kotlin/github/buriedincode/kraken/schemas/Universe.kt +++ b/src/main/kotlin/github/buriedincode/kraken/schemas/Universe.kt @@ -1,7 +1,8 @@ package github.buriedincode.kraken.schemas import github.buriedincode.kraken.serializers.NullableStringSerializer -import kotlinx.datetime.Instant +import kotlin.time.ExperimentalTime +import kotlin.time.Instant import kotlinx.serialization.ExperimentalSerializationApi import kotlinx.serialization.Serializable import kotlinx.serialization.json.JsonNames @@ -19,21 +20,16 @@ import kotlinx.serialization.json.JsonNames * @property publisher The publisher of the universe. * @property resourceUrl The URL of the universe. */ -@OptIn(ExperimentalSerializationApi::class) +@OptIn(ExperimentalSerializationApi::class, ExperimentalTime::class) @Serializable data class Universe( - @Serializable(with = NullableStringSerializer::class) - @JsonNames("desc") - val description: String? = null, - @Serializable(with = NullableStringSerializer::class) - val designation: String? = null, - @JsonNames("gcd_id") - val grandComicsDatabaseId: Long? = null, - val id: Long, - @Serializable(with = NullableStringSerializer::class) - val image: String? = null, - val modified: Instant, - val name: String, - val publisher: GenericItem, - val resourceUrl: String, + @Serializable(with = NullableStringSerializer::class) @JsonNames("desc") val description: String? = null, + @Serializable(with = NullableStringSerializer::class) val designation: String? = null, + @JsonNames("gcd_id") val grandComicsDatabaseId: Long? = null, + val id: Long, + @Serializable(with = NullableStringSerializer::class) val image: String? = null, + val modified: Instant, + val name: String, + val publisher: GenericItem, + val resourceUrl: String, ) diff --git a/src/main/kotlin/github/buriedincode/kraken/serializers/EmptyListSerializer.kt b/src/main/kotlin/github/buriedincode/kraken/serializers/EmptyListSerializer.kt index 26054b1..a1534f4 100644 --- a/src/main/kotlin/github/buriedincode/kraken/serializers/EmptyListSerializer.kt +++ b/src/main/kotlin/github/buriedincode/kraken/serializers/EmptyListSerializer.kt @@ -11,8 +11,9 @@ import kotlinx.serialization.encoding.Encoder /** * A custom serializer for handling nullable lists during serialization and deserialization. * - * This serializer ensures that `null` values are deserialized as an empty list (`List`), avoiding potential issues with nullability when working with collections. - * It can be used for fields that are expected to be lists but may sometimes be `null` in the input. + * This serializer ensures that `null` values are deserialized as an empty list (`List`), avoiding potential issues + * with nullability when working with collections. It can be used for fields that are expected to be lists but may + * sometimes be `null` in the input. * * @param T The type of elements contained within the list. * @property elementSerializer The serializer used for the individual elements in the list. @@ -31,30 +32,28 @@ import kotlinx.serialization.encoding.Encoder * ``` */ class EmptyListSerializer(private val elementSerializer: KSerializer) : KSerializer> { - /** - * The serial descriptor for the list, derived from the element serializer. - */ - @OptIn(ExperimentalSerializationApi::class) - override val descriptor: SerialDescriptor = listSerialDescriptor(elementSerializer.descriptor) + /** The serial descriptor for the list, derived from the element serializer. */ + @OptIn(ExperimentalSerializationApi::class) + override val descriptor: SerialDescriptor = listSerialDescriptor(elementSerializer.descriptor) - /** - * Deserializes a nullable list into a non-nullable list. If the input is `null`, it returns an empty list instead. - * - * @param decoder The decoder used to read the serialized input. - * @return A list of type `T`, or an empty list if the input was `null`. - */ - @OptIn(ExperimentalSerializationApi::class) - override fun deserialize(decoder: Decoder): List { - return decoder.decodeNullableSerializableValue(ListSerializer(elementSerializer)) ?: emptyList() - } + /** + * Deserializes a nullable list into a non-nullable list. If the input is `null`, it returns an empty list instead. + * + * @param decoder The decoder used to read the serialized input. + * @return A list of type `T`, or an empty list if the input was `null`. + */ + @OptIn(ExperimentalSerializationApi::class) + override fun deserialize(decoder: Decoder): List { + return decoder.decodeNullableSerializableValue(ListSerializer(elementSerializer)) ?: emptyList() + } - /** - * Serializes a list of type `T` into the desired output format. - * - * @param encoder The encoder used to write the serialized output. - * @param value The list of items to be serialized. - */ - override fun serialize(encoder: Encoder, value: List) { - encoder.encodeSerializableValue(ListSerializer(elementSerializer), value) - } + /** + * Serializes a list of type `T` into the desired output format. + * + * @param encoder The encoder used to write the serialized output. + * @param value The list of items to be serialized. + */ + override fun serialize(encoder: Encoder, value: List) { + encoder.encodeSerializableValue(ListSerializer(elementSerializer), value) + } } diff --git a/src/main/kotlin/github/buriedincode/kraken/serializers/NullableStringSerializer.kt b/src/main/kotlin/github/buriedincode/kraken/serializers/NullableStringSerializer.kt index b73b9d4..2600ad3 100644 --- a/src/main/kotlin/github/buriedincode/kraken/serializers/NullableStringSerializer.kt +++ b/src/main/kotlin/github/buriedincode/kraken/serializers/NullableStringSerializer.kt @@ -11,7 +11,8 @@ import kotlinx.serialization.encoding.Encoder /** * A custom serializer for handling nullable strings in a standardized way. * - * This serializer ensures that blank strings (e.g., `""`) are deserialized as `null`. It provides a convenient mechanism to handle string fields that may either be blank or null in the input data. + * This serializer ensures that blank strings (e.g., `""`) are deserialized as `null`. It provides a convenient + * mechanism to handle string fields that may either be blank or null in the input data. * * ### Example Usage: * ```kotlin @@ -30,34 +31,32 @@ import kotlinx.serialization.encoding.Encoder * ``` */ object NullableStringSerializer : KSerializer { - /** - * The serial descriptor for a nullable string. - */ - override val descriptor: SerialDescriptor = PrimitiveSerialDescriptor("NullableString", PrimitiveKind.STRING) + /** The serial descriptor for a nullable string. */ + override val descriptor: SerialDescriptor = PrimitiveSerialDescriptor("NullableString", PrimitiveKind.STRING) - /** - * Deserializes a string value, converting blank strings to `null`. - * - * @param decoder The decoder used to read the serialized input. - * @return A nullable string, where blank strings are represented as `null`. - */ - override fun deserialize(decoder: Decoder): String? { - val value = decoder.decodeString() - return value.ifBlank { null } - } + /** + * Deserializes a string value, converting blank strings to `null`. + * + * @param decoder The decoder used to read the serialized input. + * @return A nullable string, where blank strings are represented as `null`. + */ + override fun deserialize(decoder: Decoder): String? { + val value = decoder.decodeString() + return value.ifBlank { null } + } - /** - * Serializes a nullable string. - * - * @param encoder The encoder used to write the serialized output. - * @param value The nullable string value to be serialized. - */ - @OptIn(ExperimentalSerializationApi::class) - override fun serialize(encoder: Encoder, value: String?) { - if (value.isNullOrBlank()) { - encoder.encodeNull() - } else { - encoder.encodeString(value) - } + /** + * Serializes a nullable string. + * + * @param encoder The encoder used to write the serialized output. + * @param value The nullable string value to be serialized. + */ + @OptIn(ExperimentalSerializationApi::class) + override fun serialize(encoder: Encoder, value: String?) { + if (value.isNullOrBlank()) { + encoder.encodeNull() + } else { + encoder.encodeString(value) } + } } diff --git a/src/test/kotlin/github/buriedincode/kraken/ExceptionsTest.kt b/src/test/kotlin/github/buriedincode/kraken/ExceptionsTest.kt index 918996f..98d4ac8 100644 --- a/src/test/kotlin/github/buriedincode/kraken/ExceptionsTest.kt +++ b/src/test/kotlin/github/buriedincode/kraken/ExceptionsTest.kt @@ -1,47 +1,43 @@ package github.buriedincode.kraken +import java.time.Duration import org.junit.jupiter.api.Assertions.assertThrows import org.junit.jupiter.api.Nested import org.junit.jupiter.api.Test import org.junit.jupiter.api.TestInstance import org.junit.jupiter.api.TestInstance.Lifecycle -import java.time.Duration @TestInstance(Lifecycle.PER_CLASS) class ExceptionsTest { - @Nested - inner class Authentication { - @Test - fun `Test throwing an AuthenticationException`() { - val session = Metron(username = "Invalid", password = "Invalid", cache = null) - assertThrows(AuthenticationException::class.java) { - session.getIssue(id = 1088) - } - } + @Nested + inner class Authentication { + @Test + fun `Test throwing an AuthenticationException`() { + val session = Metron(username = "Invalid", password = "Invalid", cache = null) + assertThrows(AuthenticationException::class.java) { session.getIssue(id = 1088) } } + } - @Nested - inner class Service { - @Test - fun `Test throwing a ServiceException for a 404`() { - val username = System.getenv("METRON__USERNAME") ?: "IGNORED" - val password = System.getenv("METRON__PASSWORD") ?: "IGNORED" - val session = Metron(username = username, password = password, cache = null) - assertThrows(ServiceException::class.java) { - // val uri = session.encodeURI(endpoint = "/invalid") - val uri = session.encodeURI(endpoint = "/issue/-1") - session.getRequest(uri = uri) - } - } + @Nested + inner class Service { + @Test + fun `Test throwing a ServiceException for a 404`() { + val username = System.getenv("METRON__USERNAME") ?: "IGNORED" + val password = System.getenv("METRON__PASSWORD") ?: "IGNORED" + val session = Metron(username = username, password = password, cache = null) + assertThrows(ServiceException::class.java) { + // val uri = session.encodeURI(endpoint = "/invalid") + val uri = session.encodeURI(endpoint = "/issue/-1") + session.getRequest(uri = uri) + } + } - @Test - fun `Test throwing a ServiceException for a timeout`() { - val username = System.getenv("METRON__USERNAME") ?: "IGNORED" - val password = System.getenv("METRON__PASSWORD") ?: "IGNORED" - val session = Metron(username = username, password = password, cache = null, timeout = Duration.ofMillis(1)) - assertThrows(ServiceException::class.java) { - session.getIssue(id = 1088) - } - } + @Test + fun `Test throwing a ServiceException for a timeout`() { + val username = System.getenv("METRON__USERNAME") ?: "IGNORED" + val password = System.getenv("METRON__PASSWORD") ?: "IGNORED" + val session = Metron(username = username, password = password, cache = null, timeout = Duration.ofMillis(1)) + assertThrows(ServiceException::class.java) { session.getIssue(id = 1088) } } + } } diff --git a/src/test/kotlin/github/buriedincode/kraken/schemas/ArcTest.kt b/src/test/kotlin/github/buriedincode/kraken/schemas/ArcTest.kt index 0d04d88..43504c0 100644 --- a/src/test/kotlin/github/buriedincode/kraken/schemas/ArcTest.kt +++ b/src/test/kotlin/github/buriedincode/kraken/schemas/ArcTest.kt @@ -3,6 +3,7 @@ package github.buriedincode.kraken.schemas import github.buriedincode.kraken.Metron import github.buriedincode.kraken.SQLiteCache import github.buriedincode.kraken.ServiceException +import java.nio.file.Paths import org.junit.jupiter.api.Assertions.assertEquals import org.junit.jupiter.api.Assertions.assertNotNull import org.junit.jupiter.api.Assertions.assertNull @@ -13,59 +14,58 @@ import org.junit.jupiter.api.Test import org.junit.jupiter.api.TestInstance import org.junit.jupiter.api.TestInstance.Lifecycle import org.junit.jupiter.api.assertAll -import java.nio.file.Paths @TestInstance(Lifecycle.PER_CLASS) class ArcTest { - private val session: Metron + private val session: Metron - init { - val username = System.getenv("METRON__USERNAME") ?: "IGNORED" - val password = System.getenv("METRON__PASSWORD") ?: "IGNORED" - val cache = SQLiteCache(path = Paths.get("cache.sqlite"), expiry = null) - session = Metron(username = username, password = password, cache = cache) - } + init { + val username = System.getenv("METRON__USERNAME") ?: "IGNORED" + val password = System.getenv("METRON__PASSWORD") ?: "IGNORED" + val cache = SQLiteCache(path = Paths.get("cache.sqlite"), expiry = null) + session = Metron(username = username, password = password, cache = cache) + } - @Nested - inner class ListArcs { - @Test - fun `Test ListArcs with a valid search`() { - val results = session.listArcs(params = mapOf("name" to "Cow Race")) - assertEquals(1, results.size) - assertAll( - { assertEquals(1491, results[0].id) }, - { assertEquals("The Great Cow Race", results[0].name) }, - ) - } + @Nested + inner class ListArcs { + @Test + fun `Test ListArcs with a valid search`() { + val results = session.listArcs(params = mapOf("name" to "Cow Race")) + assertEquals(1, results.size) + assertAll({ assertEquals(1491, results[0].id) }, { assertEquals("The Great Cow Race", results[0].name) }) + } - @Test - fun `Test ListArcs with an invalid search`() { - val results = session.listArcs(params = mapOf("name" to "INVALID")) - assertTrue(results.isEmpty()) - } + @Test + fun `Test ListArcs with an invalid search`() { + val results = session.listArcs(params = mapOf("name" to "INVALID")) + assertTrue(results.isEmpty()) } + } - @Nested - inner class GetArc { - @Test - fun `Test GetArc with a valid id`() { - val result = session.getArc(id = 1491) - assertNotNull(result) - assertAll( - { assertEquals(41751, result.comicvineId) }, - { assertNull(result.grandComicsDatabaseId) }, - { assertEquals(1491, result.id) }, - { assertEquals("https://static.metron.cloud/media/arc/2024/03/07/d75aba2ca26349c89c3104690d32cc2f.jpg", result.image) }, - { assertEquals("The Great Cow Race", result.name) }, - { assertEquals("https://metron.cloud/arc/bone-the-great-cow-race/", result.resourceUrl) }, - ) - } + @Nested + inner class GetArc { + @Test + fun `Test GetArc with a valid id`() { + val result = session.getArc(id = 1491) + assertNotNull(result) + assertAll( + { assertEquals(41751, result.comicvineId) }, + { assertNull(result.grandComicsDatabaseId) }, + { assertEquals(1491, result.id) }, + { + assertEquals( + "https://static.metron.cloud/media/arc/2024/03/07/d75aba2ca26349c89c3104690d32cc2f.jpg", + result.image, + ) + }, + { assertEquals("The Great Cow Race", result.name) }, + { assertEquals("https://metron.cloud/arc/bone-the-great-cow-race/", result.resourceUrl) }, + ) + } - @Test - fun `Test GetArc with an invalid id`() { - assertThrows(ServiceException::class.java) { - session.getArc(id = -1) - } - } + @Test + fun `Test GetArc with an invalid id`() { + assertThrows(ServiceException::class.java) { session.getArc(id = -1) } } + } } diff --git a/src/test/kotlin/github/buriedincode/kraken/schemas/CharacterTest.kt b/src/test/kotlin/github/buriedincode/kraken/schemas/CharacterTest.kt index 57e758f..7f1ced2 100644 --- a/src/test/kotlin/github/buriedincode/kraken/schemas/CharacterTest.kt +++ b/src/test/kotlin/github/buriedincode/kraken/schemas/CharacterTest.kt @@ -3,6 +3,7 @@ package github.buriedincode.kraken.schemas import github.buriedincode.kraken.Metron import github.buriedincode.kraken.SQLiteCache import github.buriedincode.kraken.ServiceException +import java.nio.file.Paths import org.junit.jupiter.api.Assertions.assertEquals import org.junit.jupiter.api.Assertions.assertNotNull import org.junit.jupiter.api.Assertions.assertNull @@ -13,86 +14,76 @@ import org.junit.jupiter.api.Test import org.junit.jupiter.api.TestInstance import org.junit.jupiter.api.TestInstance.Lifecycle import org.junit.jupiter.api.assertAll -import java.nio.file.Paths @TestInstance(Lifecycle.PER_CLASS) class CharacterTest { - private val session: Metron + private val session: Metron - init { - val username = System.getenv("METRON__USERNAME") ?: "IGNORED" - val password = System.getenv("METRON__PASSWORD") ?: "IGNORED" - val cache = SQLiteCache(path = Paths.get("cache.sqlite"), expiry = null) - session = Metron(username = username, password = password, cache = cache) - } + init { + val username = System.getenv("METRON__USERNAME") ?: "IGNORED" + val password = System.getenv("METRON__PASSWORD") ?: "IGNORED" + val cache = SQLiteCache(path = Paths.get("cache.sqlite"), expiry = null) + session = Metron(username = username, password = password, cache = cache) + } - @Nested - inner class ListCharacters { - @Test - fun `Test ListCharacters with a valid search`() { - val results = session.listCharacters(params = mapOf("name" to "Smiley Bone")) - assertEquals(1, results.size) - assertAll( - { assertEquals(1234, results[0].id) }, - { assertEquals("Smiley Bone", results[0].name) }, - ) - } + @Nested + inner class ListCharacters { + @Test + fun `Test ListCharacters with a valid search`() { + val results = session.listCharacters(params = mapOf("name" to "Smiley Bone")) + assertEquals(1, results.size) + assertAll({ assertEquals(1234, results[0].id) }, { assertEquals("Smiley Bone", results[0].name) }) + } - @Test - fun `Test ListCharacters with an invalid search`() { - val results = session.listCharacters(params = mapOf("name" to "INVALID")) - assertTrue(results.isEmpty()) - } + @Test + fun `Test ListCharacters with an invalid search`() { + val results = session.listCharacters(params = mapOf("name" to "INVALID")) + assertTrue(results.isEmpty()) } + } - @Nested - inner class GetCharacter { - @Test - fun `Test GetCharacter with a valid id`() { - val result = session.getCharacter(id = 1234) - assertNotNull(result) - assertAll( - { assertTrue(result.alias.isEmpty()) }, - { assertEquals(23092, result.comicvineId) }, - { - assertAll( - { assertEquals(573, result.creators[0].id) }, - { assertEquals("Jeff Smith", result.creators[0].name) }, - ) - }, - { assertNull(result.grandComicsDatabaseId) }, - { assertEquals(1234, result.id) }, - { assertEquals("https://static.metron.cloud/media/character/2019/01/21/Smiley-Bone.jpg", result.image) }, - { assertEquals("Smiley Bone", result.name) }, - { assertEquals("https://metron.cloud/character/smiley-bone/", result.resourceUrl) }, - { assertTrue(result.teams.isEmpty()) }, - { assertTrue(result.universes.isEmpty()) }, - ) - } + @Nested + inner class GetCharacter { + @Test + fun `Test GetCharacter with a valid id`() { + val result = session.getCharacter(id = 1234) + assertNotNull(result) + assertAll( + { assertTrue(result.alias.isEmpty()) }, + { assertEquals(23092, result.comicvineId) }, + { + assertAll( + { assertEquals(573, result.creators[0].id) }, + { assertEquals("Jeff Smith", result.creators[0].name) }, + ) + }, + { assertNull(result.grandComicsDatabaseId) }, + { assertEquals(1234, result.id) }, + { assertEquals("https://static.metron.cloud/media/character/2019/01/21/Smiley-Bone.jpg", result.image) }, + { assertEquals("Smiley Bone", result.name) }, + { assertEquals("https://metron.cloud/character/smiley-bone/", result.resourceUrl) }, + { assertTrue(result.teams.isEmpty()) }, + { assertTrue(result.universes.isEmpty()) }, + ) + } - @Test - fun `Test GetCharacter with an invalid id`() { - assertThrows(ServiceException::class.java) { - session.getCharacter(id = -1) - } - } + @Test + fun `Test GetCharacter with an invalid id`() { + assertThrows(ServiceException::class.java) { session.getCharacter(id = -1) } + } - @Test - fun `Test GetCharacter with a null alias`() { - val result = session.getCharacter(id = 25657) - assertNotNull(result) - assertAll( - { assertTrue(result.alias.isEmpty()) }, - ) - } + @Test + fun `Test GetCharacter with a null alias`() { + val result = session.getCharacter(id = 25657) + assertNotNull(result) + assertAll({ assertTrue(result.alias.isEmpty()) }) + } - @Test - fun `Test GetCharacter with an alias`() { - val result = session.getCharacter(id = 648) - assertNotNull(result) - assertAll( - { assertEquals("Spy-D", result.alias[0]) }, - ) - } + @Test + fun `Test GetCharacter with an alias`() { + val result = session.getCharacter(id = 648) + assertNotNull(result) + assertAll({ assertEquals("Spy-D", result.alias[0]) }) } + } } diff --git a/src/test/kotlin/github/buriedincode/kraken/schemas/CreatorTest.kt b/src/test/kotlin/github/buriedincode/kraken/schemas/CreatorTest.kt index 70a0979..b0886a2 100644 --- a/src/test/kotlin/github/buriedincode/kraken/schemas/CreatorTest.kt +++ b/src/test/kotlin/github/buriedincode/kraken/schemas/CreatorTest.kt @@ -3,6 +3,7 @@ package github.buriedincode.kraken.schemas import github.buriedincode.kraken.Metron import github.buriedincode.kraken.SQLiteCache import github.buriedincode.kraken.ServiceException +import java.nio.file.Paths import kotlinx.datetime.LocalDate import org.junit.jupiter.api.Assertions.assertEquals import org.junit.jupiter.api.Assertions.assertNotNull @@ -14,62 +15,56 @@ import org.junit.jupiter.api.Test import org.junit.jupiter.api.TestInstance import org.junit.jupiter.api.TestInstance.Lifecycle import org.junit.jupiter.api.assertAll -import java.nio.file.Paths @TestInstance(Lifecycle.PER_CLASS) class CreatorTest { - private val session: Metron + private val session: Metron - init { - val username = System.getenv("METRON__USERNAME") ?: "IGNORED" - val password = System.getenv("METRON__PASSWORD") ?: "IGNORED" - val cache = SQLiteCache(path = Paths.get("cache.sqlite"), expiry = null) - session = Metron(username = username, password = password, cache = cache) - } + init { + val username = System.getenv("METRON__USERNAME") ?: "IGNORED" + val password = System.getenv("METRON__PASSWORD") ?: "IGNORED" + val cache = SQLiteCache(path = Paths.get("cache.sqlite"), expiry = null) + session = Metron(username = username, password = password, cache = cache) + } - @Nested - inner class ListCreators { - @Test - fun `Test ListCreators with a valid search`() { - val results = session.listCreators(params = mapOf("name" to "Jeff Smith")) - assertEquals(1, results.size) - assertAll( - { assertEquals(573, results[0].id) }, - { assertEquals("Jeff Smith", results[0].name) }, - ) - } + @Nested + inner class ListCreators { + @Test + fun `Test ListCreators with a valid search`() { + val results = session.listCreators(params = mapOf("name" to "Jeff Smith")) + assertEquals(1, results.size) + assertAll({ assertEquals(573, results[0].id) }, { assertEquals("Jeff Smith", results[0].name) }) + } - @Test - fun `Test ListCreators with an invalid search`() { - val results = session.listCreators(params = mapOf("name" to "INVALID")) - assertTrue(results.isEmpty()) - } + @Test + fun `Test ListCreators with an invalid search`() { + val results = session.listCreators(params = mapOf("name" to "INVALID")) + assertTrue(results.isEmpty()) } + } - @Nested - inner class GetCreator { - @Test - fun `Test GetCreator with a valid id`() { - val result = session.getCreator(id = 573) - assertNotNull(result) - assertAll( - { assertTrue(result.alias.isEmpty()) }, - { assertEquals(LocalDate(1960, 2, 27), result.birth) }, - { assertEquals(23088, result.comicvineId) }, - { assertNull(result.death) }, - { assertNull(result.grandComicsDatabaseId) }, - { assertEquals(573, result.id) }, - { assertEquals("https://static.metron.cloud/media/creator/2018/12/06/jeff_smith.jpg", result.image) }, - { assertEquals("Jeff Smith", result.name) }, - { assertEquals("https://metron.cloud/creator/jeff-smith/", result.resourceUrl) }, - ) - } + @Nested + inner class GetCreator { + @Test + fun `Test GetCreator with a valid id`() { + val result = session.getCreator(id = 573) + assertNotNull(result) + assertAll( + { assertTrue(result.alias.isEmpty()) }, + { assertEquals(LocalDate(1960, 2, 27), result.birth) }, + { assertEquals(23088, result.comicvineId) }, + { assertNull(result.death) }, + { assertNull(result.grandComicsDatabaseId) }, + { assertEquals(573, result.id) }, + { assertEquals("https://static.metron.cloud/media/creator/2018/12/06/jeff_smith.jpg", result.image) }, + { assertEquals("Jeff Smith", result.name) }, + { assertEquals("https://metron.cloud/creator/jeff-smith/", result.resourceUrl) }, + ) + } - @Test - fun `Test GetCreator with an invalid id`() { - assertThrows(ServiceException::class.java) { - session.getCreator(id = -1) - } - } + @Test + fun `Test GetCreator with an invalid id`() { + assertThrows(ServiceException::class.java) { session.getCreator(id = -1) } } + } } diff --git a/src/test/kotlin/github/buriedincode/kraken/schemas/ImprintTest.kt b/src/test/kotlin/github/buriedincode/kraken/schemas/ImprintTest.kt index 26a8614..9dc4527 100644 --- a/src/test/kotlin/github/buriedincode/kraken/schemas/ImprintTest.kt +++ b/src/test/kotlin/github/buriedincode/kraken/schemas/ImprintTest.kt @@ -3,6 +3,7 @@ package github.buriedincode.kraken.schemas import github.buriedincode.kraken.Metron import github.buriedincode.kraken.SQLiteCache import github.buriedincode.kraken.ServiceException +import java.nio.file.Paths import org.junit.jupiter.api.Assertions.assertEquals import org.junit.jupiter.api.Assertions.assertNotNull import org.junit.jupiter.api.Assertions.assertNull @@ -13,66 +14,57 @@ import org.junit.jupiter.api.Test import org.junit.jupiter.api.TestInstance import org.junit.jupiter.api.TestInstance.Lifecycle import org.junit.jupiter.api.assertAll -import java.nio.file.Paths @TestInstance(Lifecycle.PER_CLASS) class ImprintTest { - private val session: Metron + private val session: Metron - init { - val username = System.getenv("METRON__USERNAME") ?: "IGNORED" - val password = System.getenv("METRON__PASSWORD") ?: "IGNORED" - val cache = SQLiteCache(path = Paths.get("cache.sqlite"), expiry = null) - session = Metron(username = username, password = password, cache = cache) - } + init { + val username = System.getenv("METRON__USERNAME") ?: "IGNORED" + val password = System.getenv("METRON__PASSWORD") ?: "IGNORED" + val cache = SQLiteCache(path = Paths.get("cache.sqlite"), expiry = null) + session = Metron(username = username, password = password, cache = cache) + } - @Nested - inner class ListImprints { - @Test - fun `Test ListImprints with a valid search`() { - val results = session.listImprints(params = mapOf("name" to "KaBOOM!")) - assertEquals(1, results.size) - assertAll( - { assertEquals(12, results[0].id) }, - { assertEquals("KaBOOM!", results[0].name) }, - ) - } + @Nested + inner class ListImprints { + @Test + fun `Test ListImprints with a valid search`() { + val results = session.listImprints(params = mapOf("name" to "KaBOOM!")) + assertEquals(1, results.size) + assertAll({ assertEquals(12, results[0].id) }, { assertEquals("KaBOOM!", results[0].name) }) + } - @Test - fun `Test ListImprints with an invalid search`() { - val results = session.listImprints(params = mapOf("name" to "INVALID")) - assertTrue(results.isEmpty()) - } + @Test + fun `Test ListImprints with an invalid search`() { + val results = session.listImprints(params = mapOf("name" to "INVALID")) + assertTrue(results.isEmpty()) } + } - @Nested - inner class GetImprint { - @Test - fun `Test GetImprint with a valid id`() { - val result = session.getImprint(id = 12) - assertNotNull(result) - assertAll( - { assertNull(result.comicvineId) }, - { assertNull(result.founded) }, - { assertNull(result.grandComicsDatabaseId) }, - { assertEquals(12, result.id) }, - { assertEquals("https://static.metron.cloud/media/imprint/2024/08/13/kaboom.jpg", result.image) }, - { assertEquals("KaBOOM!", result.name) }, - { - assertAll( - { assertEquals(20, result.publisher.id) }, - { assertEquals("Boom! Studios", result.publisher.name) }, - ) - }, - { assertEquals("https://metron.cloud/imprint/kaboom/", result.resourceUrl) }, - ) - } + @Nested + inner class GetImprint { + @Test + fun `Test GetImprint with a valid id`() { + val result = session.getImprint(id = 12) + assertNotNull(result) + assertAll( + { assertNull(result.comicvineId) }, + { assertNull(result.founded) }, + { assertNull(result.grandComicsDatabaseId) }, + { assertEquals(12, result.id) }, + { assertEquals("https://static.metron.cloud/media/imprint/2024/08/13/kaboom.jpg", result.image) }, + { assertEquals("KaBOOM!", result.name) }, + { + assertAll({ assertEquals(20, result.publisher.id) }, { assertEquals("Boom! Studios", result.publisher.name) }) + }, + { assertEquals("https://metron.cloud/imprint/kaboom/", result.resourceUrl) }, + ) + } - @Test - fun `Test GetImprint with an invalid id`() { - assertThrows(ServiceException::class.java) { - session.getImprint(id = -1) - } - } + @Test + fun `Test GetImprint with an invalid id`() { + assertThrows(ServiceException::class.java) { session.getImprint(id = -1) } } + } } diff --git a/src/test/kotlin/github/buriedincode/kraken/schemas/IssueTest.kt b/src/test/kotlin/github/buriedincode/kraken/schemas/IssueTest.kt index 3124226..e6701e2 100644 --- a/src/test/kotlin/github/buriedincode/kraken/schemas/IssueTest.kt +++ b/src/test/kotlin/github/buriedincode/kraken/schemas/IssueTest.kt @@ -3,6 +3,7 @@ package github.buriedincode.kraken.schemas import github.buriedincode.kraken.Metron import github.buriedincode.kraken.SQLiteCache import github.buriedincode.kraken.ServiceException +import java.nio.file.Paths import kotlinx.datetime.LocalDate import org.junit.jupiter.api.Assertions.assertEquals import org.junit.jupiter.api.Assertions.assertNotNull @@ -14,144 +15,131 @@ import org.junit.jupiter.api.Test import org.junit.jupiter.api.TestInstance import org.junit.jupiter.api.TestInstance.Lifecycle import org.junit.jupiter.api.assertAll -import java.nio.file.Paths @TestInstance(Lifecycle.PER_CLASS) class IssueTest { - private val session: Metron + private val session: Metron - init { - val username = System.getenv("METRON__USERNAME") ?: "IGNORED" - val password = System.getenv("METRON__PASSWORD") ?: "IGNORED" - val cache = SQLiteCache(path = Paths.get("cache.sqlite"), expiry = null) - session = Metron(username = username, password = password, cache = cache) - } + init { + val username = System.getenv("METRON__USERNAME") ?: "IGNORED" + val password = System.getenv("METRON__PASSWORD") ?: "IGNORED" + val cache = SQLiteCache(path = Paths.get("cache.sqlite"), expiry = null) + session = Metron(username = username, password = password, cache = cache) + } - @Nested - inner class ListIssues { - @Test - fun `Test ListIssues with a valid search`() { - val results = session.listIssues(params = mapOf("series_id" to 119.toString(), "number" to "1")) - assertEquals(1, results.size) - assertAll( - { assertEquals(LocalDate(1991, 7, 1), results[0].coverDate) }, - { assertEquals("87386cc738ac7b38", results[0].coverHash) }, - { assertEquals(1088, results[0].id) }, - { assertEquals("https://static.metron.cloud/media/issue/2019/01/21/bone-1.jpg", results[0].image) }, - { assertEquals("Bone (1991) #1", results[0].title) }, - { assertEquals("1", results[0].number) }, - { - assertAll( - { assertEquals("Bone", results[0].series.name) }, - { assertEquals(1, results[0].series.volume) }, - { assertEquals(1991, results[0].series.yearBegan) }, - ) - }, - { assertNull(results[0].storeDate) }, - ) - } + @Nested + inner class ListIssues { + @Test + fun `Test ListIssues with a valid search`() { + val results = session.listIssues(params = mapOf("series_id" to 119.toString(), "number" to "1")) + assertEquals(1, results.size) + assertAll( + { assertEquals(LocalDate(1991, 7, 1), results[0].coverDate) }, + { assertEquals("87386cc738ac7b38", results[0].coverHash) }, + { assertEquals(1088, results[0].id) }, + { assertEquals("https://static.metron.cloud/media/issue/2019/01/21/bone-1.jpg", results[0].image) }, + { assertEquals("Bone (1991) #1", results[0].title) }, + { assertEquals("1", results[0].number) }, + { + assertAll( + { assertEquals("Bone", results[0].series.name) }, + { assertEquals(1, results[0].series.volume) }, + { assertEquals(1991, results[0].series.yearBegan) }, + ) + }, + { assertNull(results[0].storeDate) }, + ) + } - @Test - fun `Test ListIssues with an invalid search`() { - val results = session.listIssues(params = mapOf("series_id" to 119.toString(), "number" to "INVALID")) - assertTrue(results.isEmpty()) - } + @Test + fun `Test ListIssues with an invalid search`() { + val results = session.listIssues(params = mapOf("series_id" to 119.toString(), "number" to "INVALID")) + assertTrue(results.isEmpty()) } + } - @Nested - inner class GetIssue { - @Test - fun `Test GetIssue with a valid id`() { - val result = session.getIssue(id = 1088) - assertNotNull(result) - assertAll( - { assertNull(result.alternativeNumber) }, - { assertTrue(result.arcs.isEmpty()) }, - { - assertAll( - { assertEquals(1232, result.characters[0].id) }, - { assertEquals("Fone Bone", result.characters[0].name) }, - ) - }, - { assertEquals(34352, result.comicvineId) }, - { assertEquals(LocalDate(1991, 7, 1), result.coverDate) }, - { assertEquals("87386cc738ac7b38", result.coverHash) }, - { - assertAll( - { assertEquals("Jeff Smith", result.credits[0].creator) }, - { assertEquals(573, result.credits[0].id) }, - { - assertAll( - { assertEquals(1, result.credits[0].roles[0].id) }, - { assertEquals("Writer", result.credits[0].roles[0].name) }, - ) - }, - ) - }, - { assertNull(result.grandComicsDatabaseId) }, - { assertEquals(1088, result.id) }, - { assertEquals("https://static.metron.cloud/media/issue/2019/01/21/bone-1.jpg", result.image) }, - { assertNull(result.imprint) }, - { assertNull(result.isbn) }, - { assertEquals("1", result.number) }, - { assertEquals(28, result.pageCount) }, - { assertEquals(2.95, result.price) }, - { - assertAll( - { assertEquals(19, result.publisher.id) }, - { assertEquals("Cartoon Books", result.publisher.name) }, - ) - }, - { - assertAll( - { assertEquals(1, result.rating.id) }, - { assertEquals("Unknown", result.rating.name) }, - ) - }, - { - assertAll( - { assertEquals(113595, result.reprints[0].id) }, - { assertEquals("Bone TPB (2004) #1", result.reprints[0].issue) }, - ) - }, - { assertEquals("https://metron.cloud/issue/bone-1991-1/", result.resourceUrl) }, - { - assertAll( - { assertTrue(result.series.genres.isEmpty()) }, - { assertEquals(119, result.series.id) }, - { assertEquals("Bone", result.series.name) }, - { - assertAll( - { assertEquals(13, result.series.seriesType.id) }, - { assertEquals("Single Issue", result.series.seriesType.name) }, - ) - }, - { assertEquals("Bone", result.series.sortName) }, - { assertEquals(1, result.series.volume) }, - { assertEquals(1991, result.series.yearBegan) }, - ) - }, - { assertNull(result.sku) }, - { assertNull(result.storeDate) }, - { assertEquals("The Map", result.stories[0]) }, - { - assertAll( - { assertEquals(1473, result.teams[0].id) }, - { assertEquals("Rat Creatures", result.teams[0].name) }, - ) - }, - { assertNull(result.title) }, - { assertTrue(result.universes.isEmpty()) }, - { assertNull(result.upc) }, - { assertTrue(result.variants.isEmpty()) }, - ) - } + @Nested + inner class GetIssue { + @Test + fun `Test GetIssue with a valid id`() { + val result = session.getIssue(id = 1088) + assertNotNull(result) + assertAll( + { assertNull(result.alternativeNumber) }, + { assertTrue(result.arcs.isEmpty()) }, + { + assertAll( + { assertEquals(1232, result.characters[0].id) }, + { assertEquals("Fone Bone", result.characters[0].name) }, + ) + }, + { assertEquals(34352, result.comicvineId) }, + { assertEquals(LocalDate(1991, 7, 1), result.coverDate) }, + { assertEquals("87386cc738ac7b38", result.coverHash) }, + { + assertAll( + { assertEquals("Jeff Smith", result.credits[0].creator) }, + { assertEquals(573, result.credits[0].id) }, + { + assertAll( + { assertEquals(1, result.credits[0].roles[0].id) }, + { assertEquals("Writer", result.credits[0].roles[0].name) }, + ) + }, + ) + }, + { assertNull(result.focDate) }, + { assertNull(result.grandComicsDatabaseId) }, + { assertEquals(1088, result.id) }, + { assertEquals("https://static.metron.cloud/media/issue/2019/01/21/bone-1.jpg", result.image) }, + { assertNull(result.imprint) }, + { assertNull(result.isbn) }, + { assertEquals("1", result.number) }, + { assertEquals(28, result.pageCount) }, + { assertEquals(2.95, result.price) }, + { + assertAll({ assertEquals(19, result.publisher.id) }, { assertEquals("Cartoon Books", result.publisher.name) }) + }, + { assertAll({ assertEquals(1, result.rating.id) }, { assertEquals("Unknown", result.rating.name) }) }, + { + assertAll( + { assertEquals(113595, result.reprints[0].id) }, + { assertEquals("Bone TPB (2004) #1", result.reprints[0].issue) }, + ) + }, + { assertEquals("https://metron.cloud/issue/bone-1991-1/", result.resourceUrl) }, + { + assertAll( + { assertTrue(result.series.genres.isEmpty()) }, + { assertEquals(119, result.series.id) }, + { assertEquals("Bone", result.series.name) }, + { + assertAll( + { assertEquals(13, result.series.seriesType.id) }, + { assertEquals("Single Issue", result.series.seriesType.name) }, + ) + }, + { assertEquals("Bone", result.series.sortName) }, + { assertEquals(1, result.series.volume) }, + { assertEquals(1991, result.series.yearBegan) }, + ) + }, + { assertNull(result.sku) }, + { assertNull(result.storeDate) }, + { assertEquals("The Map", result.stories[0]) }, + { + assertAll({ assertEquals(1473, result.teams[0].id) }, { assertEquals("Rat Creatures", result.teams[0].name) }) + }, + { assertNull(result.title) }, + { assertTrue(result.universes.isEmpty()) }, + { assertNull(result.upc) }, + { assertTrue(result.variants.isEmpty()) }, + ) + } - @Test - fun `Test GetIssue with an invalid id`() { - assertThrows(ServiceException::class.java) { - session.getIssue(id = -1) - } - } + @Test + fun `Test GetIssue with an invalid id`() { + assertThrows(ServiceException::class.java) { session.getIssue(id = -1) } } + } } diff --git a/src/test/kotlin/github/buriedincode/kraken/schemas/PublisherTest.kt b/src/test/kotlin/github/buriedincode/kraken/schemas/PublisherTest.kt index 22f0d51..3104c22 100644 --- a/src/test/kotlin/github/buriedincode/kraken/schemas/PublisherTest.kt +++ b/src/test/kotlin/github/buriedincode/kraken/schemas/PublisherTest.kt @@ -3,6 +3,7 @@ package github.buriedincode.kraken.schemas import github.buriedincode.kraken.Metron import github.buriedincode.kraken.SQLiteCache import github.buriedincode.kraken.ServiceException +import java.nio.file.Paths import org.junit.jupiter.api.Assertions.assertEquals import org.junit.jupiter.api.Assertions.assertNotNull import org.junit.jupiter.api.Assertions.assertNull @@ -13,61 +14,55 @@ import org.junit.jupiter.api.Test import org.junit.jupiter.api.TestInstance import org.junit.jupiter.api.TestInstance.Lifecycle import org.junit.jupiter.api.assertAll -import java.nio.file.Paths @TestInstance(Lifecycle.PER_CLASS) class PublisherTest { - private val session: Metron + private val session: Metron - init { - val username = System.getenv("METRON__USERNAME") ?: "IGNORED" - val password = System.getenv("METRON__PASSWORD") ?: "IGNORED" - val cache = SQLiteCache(path = Paths.get("cache.sqlite"), expiry = null) - session = Metron(username = username, password = password, cache = cache) - } + init { + val username = System.getenv("METRON__USERNAME") ?: "IGNORED" + val password = System.getenv("METRON__PASSWORD") ?: "IGNORED" + val cache = SQLiteCache(path = Paths.get("cache.sqlite"), expiry = null) + session = Metron(username = username, password = password, cache = cache) + } - @Nested - inner class ListPublishers { - @Test - fun `Test ListPublishers with a valid search`() { - val results = session.listPublishers(params = mapOf("name" to "Cartoon Books")) - assertEquals(1, results.size) - assertAll( - { assertEquals(19, results[0].id) }, - { assertEquals("Cartoon Books", results[0].name) }, - ) - } + @Nested + inner class ListPublishers { + @Test + fun `Test ListPublishers with a valid search`() { + val results = session.listPublishers(params = mapOf("name" to "Cartoon Books")) + assertEquals(1, results.size) + assertAll({ assertEquals(19, results[0].id) }, { assertEquals("Cartoon Books", results[0].name) }) + } - @Test - fun `Test ListPublishers with an invalid search`() { - val results = session.listPublishers(params = mapOf("name" to "INVALID")) - assertTrue(results.isEmpty()) - } + @Test + fun `Test ListPublishers with an invalid search`() { + val results = session.listPublishers(params = mapOf("name" to "INVALID")) + assertTrue(results.isEmpty()) } + } - @Nested - inner class GetPublisher { - @Test - fun `Test GetPublisher with a valid id`() { - val result = session.getPublisher(id = 19) - assertNotNull(result) - assertAll( - { assertEquals(490, result.comicvineId) }, - { assertEquals("US", result.country) }, - { assertEquals(1991, result.founded) }, - { assertNull(result.grandComicsDatabaseId) }, - { assertEquals(19, result.id) }, - { assertEquals("https://static.metron.cloud/media/publisher/2019/01/21/cartoon-books.jpg", result.image) }, - { assertEquals("Cartoon Books", result.name) }, - { assertEquals("https://metron.cloud/publisher/cartoon-books/", result.resourceUrl) }, - ) - } + @Nested + inner class GetPublisher { + @Test + fun `Test GetPublisher with a valid id`() { + val result = session.getPublisher(id = 19) + assertNotNull(result) + assertAll( + { assertEquals(490, result.comicvineId) }, + { assertEquals("US", result.country) }, + { assertEquals(1991, result.founded) }, + { assertNull(result.grandComicsDatabaseId) }, + { assertEquals(19, result.id) }, + { assertEquals("https://static.metron.cloud/media/publisher/2019/01/21/cartoon-books.jpg", result.image) }, + { assertEquals("Cartoon Books", result.name) }, + { assertEquals("https://metron.cloud/publisher/cartoon-books/", result.resourceUrl) }, + ) + } - @Test - fun `Test GetPublisher with an invalid id`() { - assertThrows(ServiceException::class.java) { - session.getPublisher(id = -1) - } - } + @Test + fun `Test GetPublisher with an invalid id`() { + assertThrows(ServiceException::class.java) { session.getPublisher(id = -1) } } + } } diff --git a/src/test/kotlin/github/buriedincode/kraken/schemas/RoleTest.kt b/src/test/kotlin/github/buriedincode/kraken/schemas/RoleTest.kt index ad9abc1..78e867a 100644 --- a/src/test/kotlin/github/buriedincode/kraken/schemas/RoleTest.kt +++ b/src/test/kotlin/github/buriedincode/kraken/schemas/RoleTest.kt @@ -2,6 +2,7 @@ package github.buriedincode.kraken.schemas import github.buriedincode.kraken.Metron import github.buriedincode.kraken.SQLiteCache +import java.nio.file.Paths import org.junit.jupiter.api.Assertions.assertEquals import org.junit.jupiter.api.Assertions.assertTrue import org.junit.jupiter.api.Nested @@ -9,35 +10,31 @@ import org.junit.jupiter.api.Test import org.junit.jupiter.api.TestInstance import org.junit.jupiter.api.TestInstance.Lifecycle import org.junit.jupiter.api.assertAll -import java.nio.file.Paths @TestInstance(Lifecycle.PER_CLASS) class RoleTest { - private val session: Metron + private val session: Metron - init { - val username = System.getenv("METRON__USERNAME") ?: "IGNORED" - val password = System.getenv("METRON__PASSWORD") ?: "IGNORED" - val cache = SQLiteCache(path = Paths.get("cache.sqlite"), expiry = null) - session = Metron(username = username, password = password, cache = cache) - } + init { + val username = System.getenv("METRON__USERNAME") ?: "IGNORED" + val password = System.getenv("METRON__PASSWORD") ?: "IGNORED" + val cache = SQLiteCache(path = Paths.get("cache.sqlite"), expiry = null) + session = Metron(username = username, password = password, cache = cache) + } - @Nested - inner class ListRoles { - @Test - fun `Test ListRoles with a valid search`() { - val results = session.listRoles(params = mapOf("name" to "Writer")) - assertEquals(1, results.size) - assertAll( - { assertEquals(1, results[0].id) }, - { assertEquals("Writer", results[0].name) }, - ) - } + @Nested + inner class ListRoles { + @Test + fun `Test ListRoles with a valid search`() { + val results = session.listRoles(params = mapOf("name" to "Writer")) + assertEquals(1, results.size) + assertAll({ assertEquals(1, results[0].id) }, { assertEquals("Writer", results[0].name) }) + } - @Test - fun `Test ListRoles with an invalid search`() { - val results = session.listRoles(params = mapOf("name" to "INVALID")) - assertTrue(results.isEmpty()) - } + @Test + fun `Test ListRoles with an invalid search`() { + val results = session.listRoles(params = mapOf("name" to "INVALID")) + assertTrue(results.isEmpty()) } + } } diff --git a/src/test/kotlin/github/buriedincode/kraken/schemas/SeriesTest.kt b/src/test/kotlin/github/buriedincode/kraken/schemas/SeriesTest.kt index 5f25614..eb0abb6 100644 --- a/src/test/kotlin/github/buriedincode/kraken/schemas/SeriesTest.kt +++ b/src/test/kotlin/github/buriedincode/kraken/schemas/SeriesTest.kt @@ -3,6 +3,7 @@ package github.buriedincode.kraken.schemas import github.buriedincode.kraken.Metron import github.buriedincode.kraken.SQLiteCache import github.buriedincode.kraken.ServiceException +import java.nio.file.Paths import org.junit.jupiter.api.Assertions.assertEquals import org.junit.jupiter.api.Assertions.assertNotNull import org.junit.jupiter.api.Assertions.assertNull @@ -13,82 +14,76 @@ import org.junit.jupiter.api.Test import org.junit.jupiter.api.TestInstance import org.junit.jupiter.api.TestInstance.Lifecycle import org.junit.jupiter.api.assertAll -import java.nio.file.Paths @TestInstance(Lifecycle.PER_CLASS) class SeriesTest { - private val session: Metron + private val session: Metron - init { - val username = System.getenv("METRON__USERNAME") ?: "IGNORED" - val password = System.getenv("METRON__PASSWORD") ?: "IGNORED" - val cache = SQLiteCache(path = Paths.get("cache.sqlite"), expiry = null) - session = Metron(username = username, password = password, cache = cache) - } + init { + val username = System.getenv("METRON__USERNAME") ?: "IGNORED" + val password = System.getenv("METRON__PASSWORD") ?: "IGNORED" + val cache = SQLiteCache(path = Paths.get("cache.sqlite"), expiry = null) + session = Metron(username = username, password = password, cache = cache) + } - @Nested - inner class ListSeries { - @Test - fun `Test ListSeries with a valid search`() { - val results = session.listSeries(params = mapOf("name" to "Bone")) - assertEquals(12, results.size) - assertAll( - { assertEquals(119, results[0].id) }, - { assertEquals(56, results[0].issueCount) }, - { assertEquals("Bone (1991)", results[0].name) }, - { assertEquals(1, results[0].volume) }, - { assertEquals(1991, results[0].yearBegan) }, - ) - } + @Nested + inner class ListSeries { + @Test + fun `Test ListSeries with a valid search`() { + val results = session.listSeries(params = mapOf("name" to "Bone")) + assertEquals(16, results.size) + assertAll( + { assertEquals(119, results[0].id) }, + { assertEquals(56, results[0].issueCount) }, + { assertEquals("Bone (1991)", results[0].name) }, + { assertEquals(1, results[0].volume) }, + { assertEquals(1991, results[0].yearBegan) }, + ) + } - @Test - fun `Test ListSeries with an invalid search`() { - val results = session.listSeries(params = mapOf("name" to "INVALID")) - assertTrue(results.isEmpty()) - } + @Test + fun `Test ListSeries with an invalid search`() { + val results = session.listSeries(params = mapOf("name" to "INVALID")) + assertTrue(results.isEmpty()) } + } - @Nested - inner class GetSeries { - @Test - fun `Test GetSeries with a valid id`() { - val result = session.getSeries(id = 119) - assertNotNull(result) - assertAll( - { assertTrue(result.associated.isEmpty()) }, - { assertEquals(4691, result.comicvineId) }, - { assertTrue(result.genres.isEmpty()) }, - { assertNull(result.grandComicsDatabaseId) }, - { assertEquals(119, result.id) }, - { assertNull(result.imprint) }, - { assertEquals(56, result.issueCount) }, - { assertEquals("Bone", result.name) }, - { - assertAll( - { assertEquals(19, result.publisher.id) }, - { assertEquals("Cartoon Books", result.publisher.name) }, - ) - }, - { assertEquals("https://metron.cloud/series/bone-1991/", result.resourceUrl) }, - { - assertAll( - { assertEquals(13, result.seriesType.id) }, - { assertEquals("Single Issue", result.seriesType.name) }, - ) - }, - { assertEquals("Cancelled", result.status) }, - { assertEquals("Bone", result.sortName) }, - { assertEquals(1, result.volume) }, - { assertEquals(1991, result.yearBegan) }, - { assertEquals(1995, result.yearEnd) }, - ) - } + @Nested + inner class GetSeries { + @Test + fun `Test GetSeries with a valid id`() { + val result = session.getSeries(id = 119) + assertNotNull(result) + assertAll( + { assertTrue(result.associated.isEmpty()) }, + { assertEquals(4691, result.comicvineId) }, + { assertTrue(result.genres.isEmpty()) }, + { assertNull(result.grandComicsDatabaseId) }, + { assertEquals(119, result.id) }, + { assertNull(result.imprint) }, + { assertEquals(56, result.issueCount) }, + { assertEquals("Bone", result.name) }, + { + assertAll({ assertEquals(19, result.publisher.id) }, { assertEquals("Cartoon Books", result.publisher.name) }) + }, + { assertEquals("https://metron.cloud/series/bone-1991/", result.resourceUrl) }, + { + assertAll( + { assertEquals(13, result.seriesType.id) }, + { assertEquals("Single Issue", result.seriesType.name) }, + ) + }, + { assertEquals("Cancelled", result.status) }, + { assertEquals("Bone", result.sortName) }, + { assertEquals(1, result.volume) }, + { assertEquals(1991, result.yearBegan) }, + { assertEquals(1995, result.yearEnd) }, + ) + } - @Test - fun `Test GetSeries with an invalid id`() { - assertThrows(ServiceException::class.java) { - session.getSeries(id = -1) - } - } + @Test + fun `Test GetSeries with an invalid id`() { + assertThrows(ServiceException::class.java) { session.getSeries(id = -1) } } + } } diff --git a/src/test/kotlin/github/buriedincode/kraken/schemas/SeriesTypeTest.kt b/src/test/kotlin/github/buriedincode/kraken/schemas/SeriesTypeTest.kt index ae8e482..d2dab8d 100644 --- a/src/test/kotlin/github/buriedincode/kraken/schemas/SeriesTypeTest.kt +++ b/src/test/kotlin/github/buriedincode/kraken/schemas/SeriesTypeTest.kt @@ -2,6 +2,7 @@ package github.buriedincode.kraken.schemas import github.buriedincode.kraken.Metron import github.buriedincode.kraken.SQLiteCache +import java.nio.file.Paths import org.junit.jupiter.api.Assertions.assertEquals import org.junit.jupiter.api.Assertions.assertTrue import org.junit.jupiter.api.Nested @@ -9,35 +10,31 @@ import org.junit.jupiter.api.Test import org.junit.jupiter.api.TestInstance import org.junit.jupiter.api.TestInstance.Lifecycle import org.junit.jupiter.api.assertAll -import java.nio.file.Paths @TestInstance(Lifecycle.PER_CLASS) class SeriesTypeTest { - private val session: Metron + private val session: Metron - init { - val username = System.getenv("METRON__USERNAME") ?: "IGNORED" - val password = System.getenv("METRON__PASSWORD") ?: "IGNORED" - val cache = SQLiteCache(path = Paths.get("cache.sqlite"), expiry = null) - session = Metron(username = username, password = password, cache = cache) - } + init { + val username = System.getenv("METRON__USERNAME") ?: "IGNORED" + val password = System.getenv("METRON__PASSWORD") ?: "IGNORED" + val cache = SQLiteCache(path = Paths.get("cache.sqlite"), expiry = null) + session = Metron(username = username, password = password, cache = cache) + } - @Nested - inner class ListSeriesTypes { - @Test - fun `Test ListSeriesTypes with a valid search`() { - val results = session.listSeriesTypes(params = mapOf("name" to "Single Issue")) - assertEquals(1, results.size) - assertAll( - { assertEquals(13, results[0].id) }, - { assertEquals("Single Issue", results[0].name) }, - ) - } + @Nested + inner class ListSeriesTypes { + @Test + fun `Test ListSeriesTypes with a valid search`() { + val results = session.listSeriesTypes(params = mapOf("name" to "Single Issue")) + assertEquals(1, results.size) + assertAll({ assertEquals(13, results[0].id) }, { assertEquals("Single Issue", results[0].name) }) + } - @Test - fun `Test ListSeriesTypes with an invalid search`() { - val results = session.listSeriesTypes(params = mapOf("name" to "INVALID")) - assertTrue(results.isEmpty()) - } + @Test + fun `Test ListSeriesTypes with an invalid search`() { + val results = session.listSeriesTypes(params = mapOf("name" to "INVALID")) + assertTrue(results.isEmpty()) } + } } diff --git a/src/test/kotlin/github/buriedincode/kraken/schemas/TeamTest.kt b/src/test/kotlin/github/buriedincode/kraken/schemas/TeamTest.kt index 0493f06..63e2fcf 100644 --- a/src/test/kotlin/github/buriedincode/kraken/schemas/TeamTest.kt +++ b/src/test/kotlin/github/buriedincode/kraken/schemas/TeamTest.kt @@ -3,6 +3,7 @@ package github.buriedincode.kraken.schemas import github.buriedincode.kraken.Metron import github.buriedincode.kraken.SQLiteCache import github.buriedincode.kraken.ServiceException +import java.nio.file.Paths import org.junit.jupiter.api.Assertions.assertEquals import org.junit.jupiter.api.Assertions.assertNotNull import org.junit.jupiter.api.Assertions.assertNull @@ -13,61 +14,60 @@ import org.junit.jupiter.api.Test import org.junit.jupiter.api.TestInstance import org.junit.jupiter.api.TestInstance.Lifecycle import org.junit.jupiter.api.assertAll -import java.nio.file.Paths @TestInstance(Lifecycle.PER_CLASS) class TeamTest { - private val session: Metron + private val session: Metron - init { - val username = System.getenv("METRON__USERNAME") ?: "IGNORED" - val password = System.getenv("METRON__PASSWORD") ?: "IGNORED" - val cache = SQLiteCache(path = Paths.get("cache.sqlite"), expiry = null) - session = Metron(username = username, password = password, cache = cache) - } + init { + val username = System.getenv("METRON__USERNAME") ?: "IGNORED" + val password = System.getenv("METRON__PASSWORD") ?: "IGNORED" + val cache = SQLiteCache(path = Paths.get("cache.sqlite"), expiry = null) + session = Metron(username = username, password = password, cache = cache) + } - @Nested - inner class ListTeams { - @Test - fun `Test ListTeams with a valid search`() { - val results = session.listTeams(params = mapOf("name" to "Rat Creatures")) - assertEquals(1, results.size) - assertAll( - { assertEquals(1473, results[0].id) }, - { assertEquals("Rat Creatures", results[0].name) }, - ) - } + @Nested + inner class ListTeams { + @Test + fun `Test ListTeams with a valid search`() { + val results = session.listTeams(params = mapOf("name" to "Rat Creatures")) + assertEquals(1, results.size) + assertAll({ assertEquals(1473, results[0].id) }, { assertEquals("Rat Creatures", results[0].name) }) + } - @Test - fun `Test ListTeams with an invalid search`() { - val results = session.listTeams(params = mapOf("name" to "INVALID")) - assertTrue(results.isEmpty()) - } + @Test + fun `Test ListTeams with an invalid search`() { + val results = session.listTeams(params = mapOf("name" to "INVALID")) + assertTrue(results.isEmpty()) } + } - @Nested - inner class GetTeam { - @Test - fun `Test GetTeam with a valid id`() { - val result = session.getTeam(id = 1473) - assertNotNull(result) - assertAll( - { assertEquals(62250, result.comicvineId) }, - { assertTrue(result.creators.isEmpty()) }, - { assertNull(result.grandComicsDatabaseId) }, - { assertEquals(1473, result.id) }, - { assertEquals("https://static.metron.cloud/media/team/2024/03/07/f957fc534c0245abafbecb5e8bb4dafa.jpg", result.image) }, - { assertEquals("Rat Creatures", result.name) }, - { assertEquals("https://metron.cloud/team/rat-creatures/", result.resourceUrl) }, - { assertTrue(result.universes.isEmpty()) }, - ) - } + @Nested + inner class GetTeam { + @Test + fun `Test GetTeam with a valid id`() { + val result = session.getTeam(id = 1473) + assertNotNull(result) + assertAll( + { assertEquals(62250, result.comicvineId) }, + { assertTrue(result.creators.isEmpty()) }, + { assertNull(result.grandComicsDatabaseId) }, + { assertEquals(1473, result.id) }, + { + assertEquals( + "https://static.metron.cloud/media/team/2024/03/07/f957fc534c0245abafbecb5e8bb4dafa.jpg", + result.image, + ) + }, + { assertEquals("Rat Creatures", result.name) }, + { assertEquals("https://metron.cloud/team/rat-creatures/", result.resourceUrl) }, + { assertTrue(result.universes.isEmpty()) }, + ) + } - @Test - fun `Test GetTeam with an invalid id`() { - assertThrows(ServiceException::class.java) { - session.getTeam(id = -1) - } - } + @Test + fun `Test GetTeam with an invalid id`() { + assertThrows(ServiceException::class.java) { session.getTeam(id = -1) } } + } } diff --git a/src/test/kotlin/github/buriedincode/kraken/schemas/UniverseTest.kt b/src/test/kotlin/github/buriedincode/kraken/schemas/UniverseTest.kt index d416b5c..781da7d 100644 --- a/src/test/kotlin/github/buriedincode/kraken/schemas/UniverseTest.kt +++ b/src/test/kotlin/github/buriedincode/kraken/schemas/UniverseTest.kt @@ -3,6 +3,7 @@ package github.buriedincode.kraken.schemas import github.buriedincode.kraken.Metron import github.buriedincode.kraken.SQLiteCache import github.buriedincode.kraken.ServiceException +import java.nio.file.Paths import org.junit.jupiter.api.Assertions.assertEquals import org.junit.jupiter.api.Assertions.assertNotNull import org.junit.jupiter.api.Assertions.assertNull @@ -13,65 +14,54 @@ import org.junit.jupiter.api.Test import org.junit.jupiter.api.TestInstance import org.junit.jupiter.api.TestInstance.Lifecycle import org.junit.jupiter.api.assertAll -import java.nio.file.Paths @TestInstance(Lifecycle.PER_CLASS) class UniverseTest { - private val session: Metron + private val session: Metron - init { - val username = System.getenv("METRON__USERNAME") ?: "IGNORED" - val password = System.getenv("METRON__PASSWORD") ?: "IGNORED" - val cache = SQLiteCache(path = Paths.get("cache.sqlite"), expiry = null) - session = Metron(username = username, password = password, cache = cache) - } + init { + val username = System.getenv("METRON__USERNAME") ?: "IGNORED" + val password = System.getenv("METRON__PASSWORD") ?: "IGNORED" + val cache = SQLiteCache(path = Paths.get("cache.sqlite"), expiry = null) + session = Metron(username = username, password = password, cache = cache) + } - @Nested - inner class ListUniverses { - @Test - fun `Test ListUniverses with a valid search`() { - val results = session.listUniverses(params = mapOf("name" to "Earth 2")) - assertEquals(5, results.size) - assertAll( - { assertEquals(18, results[0].id) }, - { assertEquals("Earth 2", results[0].name) }, - ) - } + @Nested + inner class ListUniverses { + @Test + fun `Test ListUniverses with a valid search`() { + val results = session.listUniverses(params = mapOf("name" to "Earth 2")) + assertEquals(6, results.size) + assertAll({ assertEquals(18, results[0].id) }, { assertEquals("Earth 2", results[0].name) }) + } - @Test - fun `Test ListUniverses with an invalid search`() { - val results = session.listUniverses(params = mapOf("name" to "INVALID")) - assertTrue(results.isEmpty()) - } + @Test + fun `Test ListUniverses with an invalid search`() { + val results = session.listUniverses(params = mapOf("name" to "INVALID")) + assertTrue(results.isEmpty()) } + } - @Nested - inner class GetUniverse { - @Test - fun `Test GetUniverse with a valid id`() { - val result = session.getUniverse(id = 18) - assertNotNull(result) - assertAll( - { assertEquals("Earth 2", result.designation) }, - { assertNull(result.grandComicsDatabaseId) }, - { assertEquals(18, result.id) }, - { assertEquals("https://static.metron.cloud/media/universe/2024/01/25/earth-2.webp", result.image) }, - { assertEquals("Earth 2", result.name) }, - { - assertAll( - { assertEquals(2, result.publisher.id) }, - { assertEquals("DC Comics", result.publisher.name) }, - ) - }, - { assertEquals("https://metron.cloud/universe/earth-2/", result.resourceUrl) }, - ) - } + @Nested + inner class GetUniverse { + @Test + fun `Test GetUniverse with a valid id`() { + val result = session.getUniverse(id = 18) + assertNotNull(result) + assertAll( + { assertEquals("Earth 2", result.designation) }, + { assertNull(result.grandComicsDatabaseId) }, + { assertEquals(18, result.id) }, + { assertEquals("https://static.metron.cloud/media/universe/2024/01/25/earth-2.webp", result.image) }, + { assertEquals("Earth 2", result.name) }, + { assertAll({ assertEquals(2, result.publisher.id) }, { assertEquals("DC Comics", result.publisher.name) }) }, + { assertEquals("https://metron.cloud/universe/earth-2/", result.resourceUrl) }, + ) + } - @Test - fun `Test GetUniverse with an invalid id`() { - assertThrows(ServiceException::class.java) { - session.getUniverse(id = -1) - } - } + @Test + fun `Test GetUniverse with an invalid id`() { + assertThrows(ServiceException::class.java) { session.getUniverse(id = -1) } } + } }