diff --git a/.github/workflows/docs.yaml b/.github/workflows/docs.yaml index cb181fe..80373d9 100644 --- a/.github/workflows/docs.yaml +++ b/.github/workflows/docs.yaml @@ -1,4 +1,4 @@ -name: Deploy dokka to Github Pages +name: Deploy docs on: push: diff --git a/README.md b/README.md index df52511..ac0381b 100644 --- a/README.md +++ b/README.md @@ -4,7 +4,7 @@ ![Kotlin Version](https://img.shields.io/badge/Kotlin-2.1.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.11.1-informational?style=flat-square&logo=gradle)](https://github.com/gradle/gradle) +[![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) [![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) @@ -12,10 +12,11 @@ [![Github - Contributors](https://img.shields.io/github/contributors/Buried-In-Code/Kraken?logo=Github&label=Contributors&style=flat-square)](https://github.com/Buried-In-Code/Kraken/graphs/contributors) [![Github Action - Testing](https://img.shields.io/github/actions/workflow/status/Buried-In-Code/Kraken/testing.yaml?branch=main&logo=githubactions&label=Testing&style=flat-square)](https://github.com/Buried-In-Code/Kraken/actions/workflows/testing.yaml) +[![Github Action - Documentation](https://img.shields.io/github/actions/workflow/status/Buried-In-Code/Kraken/docs.yaml?branch=main&logo=githubactions&label=Documentation&style=flat-square)](https://github.com/Buried-In-Code/Kraken/actions/workflows/docs.yaml) A Java/Kotlin wrapper for the [Metron](https://metron.cloud) API. -## Getting started +## Installation To get started with Kraken, add the [JitPack](https://jitpack.io) repository to your `build.gradle.kts`. @@ -33,7 +34,7 @@ dependencies { } ``` -### Usage +### Example Usage ```kt import github.buriedincode.kraken.Metron @@ -69,6 +70,15 @@ fun main() { } ``` +## Documentation + +- [Kraken](https://buried-in-code.github.io/Kraken) +- [Metron API](https://metron.cloud/docs/) + +## Bugs/Requests + +Please use the [GitHub issue tracker](https://github.com/Buried-In-Code/Kraken/issues) to submit bugs or request features. + ## Socials [![Social - Fosstodon](https://img.shields.io/badge/%40BuriedInCode-teal?label=Fosstodon&logo=mastodon&style=for-the-badge)](https://fosstodon.org/@BuriedInCode)\ diff --git a/build.gradle.kts b/build.gradle.kts index e148822..c65dbdd 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -19,7 +19,7 @@ println("Java v${System.getProperty("java.version")}") println("Arch: ${System.getProperty("os.arch")}") group = "github.buriedincode" -version = "0.2.3" +version = "0.3.0" repositories { mavenCentral() diff --git a/src/main/kotlin/github/buriedincode/kraken/Exceptions.kt b/src/main/kotlin/github/buriedincode/kraken/Exceptions.kt index a8e261e..d9d0f06 100644 --- a/src/main/kotlin/github/buriedincode/kraken/Exceptions.kt +++ b/src/main/kotlin/github/buriedincode/kraken/Exceptions.kt @@ -1,7 +1,27 @@ package github.buriedincode.kraken +/** + * A generic exception representing any error in Kraken or the Metron service. + * + * This exception serves as the base class for all service-related exceptions within the Kraken library. + * + * @param message An optional message describing the exception. + * @param cause An optional cause that triggered this exception. + */ open class ServiceException(message: String? = null, cause: Throwable? = null) : Exception(message, cause) +/** + * An exception indicating an authentication failure with the Metron API. + * + * @param message An optional message describing the exception. + * @param cause An optional cause that triggered this exception. + */ class AuthenticationException(message: String? = null, cause: Throwable? = null) : ServiceException(message, cause) +/** + * An exception indicating that the Metron API rate limit has been exceeded. + * + * @param message An optional message describing the exception. + * @param cause An optional cause that triggered this exception. + */ class RateLimitException(message: String? = null, cause: Throwable? = null) : ServiceException(message, cause) diff --git a/src/main/kotlin/github/buriedincode/kraken/Metron.kt b/src/main/kotlin/github/buriedincode/kraken/Metron.kt index bc79a08..8d840d7 100644 --- a/src/main/kotlin/github/buriedincode/kraken/Metron.kt +++ b/src/main/kotlin/github/buriedincode/kraken/Metron.kt @@ -41,6 +41,19 @@ import kotlin.text.isNotEmpty import kotlin.text.toByteArray import kotlin.text.toInt +/** + * 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. + */ class Metron( username: String, password: String, @@ -55,13 +68,6 @@ class Metron( .build() private val authorization: String = "Basic " + Base64.getEncoder().encodeToString("$username:$password".toByteArray()) - 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) private fun performGetRequest(uri: URI): String { var attempt = 0 @@ -129,7 +135,7 @@ class Metron( this.cache.delete(url = uri.toString()) } } - val response = this.performGetRequest(uri = uri) + val response = performGetRequest(uri = uri) this.cache?.insert(url = uri.toString(), response = response) return try { JSON.decodeFromString(response) @@ -138,14 +144,21 @@ class Metron( } } + 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) - private inline fun fetchList(endpoint: String, params: Map): List { + internal inline fun fetchList(endpoint: String, params: Map): List { val resultList = mutableListOf() var page = params.getOrDefault("page", "1").toInt() do { - val uri = this.encodeURI(endpoint = endpoint, params = params + ("page" to page.toString())) - val response = this.getRequest>(uri = uri) + 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) @@ -154,89 +167,269 @@ class Metron( } @Throws(ServiceException::class, AuthenticationException::class, RateLimitException::class) - private inline fun fetchItem(endpoint: String): T = this.getRequest(uri = this.encodeURI(endpoint = endpoint)) - + 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 this.fetchList(endpoint = "/arc", params = params) + 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 = this.fetchItem(endpoint = "/arc/$id") - + 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 this.fetchList(endpoint = "/character", params = params) + 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 = this.fetchItem(endpoint = "/character/$id") - + 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 this.fetchList(endpoint = "/creator", params = params) + 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 = this.fetchItem(endpoint = "/creator/$id") - + 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 this.fetchList(endpoint = "/issue", params = params) + 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 = this.fetchItem(endpoint = "/issue/$id") - + 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 this.fetchList(endpoint = "/publisher", params = params) + 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 = this.fetchItem(endpoint = "/publisher/$id") - + 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 this.fetchList(endpoint = "/role", params = params) + 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 this.fetchList(endpoint = "/series", params = params) + 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 = this.fetchItem(endpoint = "/series/$id") - + 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 this.fetchList(endpoint = "/series_type", params = params) + 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 this.fetchList(endpoint = "/team", params = params) + 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 = this.fetchItem(endpoint = "/team/$id") - + 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 this.fetchList(endpoint = "/universe", params = params) + 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 = this.fetchItem(endpoint = "/universe/$id") + fun getUniverse(id: Long): Universe = fetchItem(endpoint = "/universe/$id") companion object { @JvmStatic diff --git a/src/main/kotlin/github/buriedincode/kraken/SQLiteCache.kt b/src/main/kotlin/github/buriedincode/kraken/SQLiteCache.kt index 06ff345..dbf0444 100644 --- a/src/main/kotlin/github/buriedincode/kraken/SQLiteCache.kt +++ b/src/main/kotlin/github/buriedincode/kraken/SQLiteCache.kt @@ -5,6 +5,15 @@ import java.sql.Date import java.sql.DriverManager 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. + * + * @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" @@ -13,6 +22,9 @@ data class SQLiteCache(val path: Path, val expiry: Int? = null) { 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 { @@ -22,6 +34,14 @@ data class SQLiteCache(val path: Path, val expiry: Int? = null) { } } + /** + * 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 = ?;" @@ -41,6 +61,14 @@ data class SQLiteCache(val path: Path, val expiry: Int? = null) { } } + /** + * 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 @@ -56,6 +84,11 @@ data class SQLiteCache(val path: Path, val expiry: Int? = null) { } } + /** + * 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 { @@ -66,6 +99,11 @@ data class SQLiteCache(val path: Path, val expiry: Int? = null) { } } + /** + * 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 diff --git a/src/main/kotlin/github/buriedincode/kraken/Utils.kt b/src/main/kotlin/github/buriedincode/kraken/Utils.kt index 4076dc4..5d1fe91 100644 --- a/src/main/kotlin/github/buriedincode/kraken/Utils.kt +++ b/src/main/kotlin/github/buriedincode/kraken/Utils.kt @@ -3,6 +3,15 @@ package github.buriedincode.kraken import io.github.oshai.kotlinlogging.KLogger 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]. + * + * @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. + */ internal fun KLogger.log(level: Level, message: () -> Any?) { when (level) { Level.TRACE -> this.trace(message) @@ -14,4 +23,7 @@ internal fun KLogger.log(level: Level, message: () -> Any?) { } } -internal const val VERSION = "0.2.3" +/** + * The version of the Kraken library. + */ +internal const val VERSION = "0.3.0" diff --git a/src/main/kotlin/github/buriedincode/kraken/schemas/Arc.kt b/src/main/kotlin/github/buriedincode/kraken/schemas/Arc.kt index 342af34..82c4042 100644 --- a/src/main/kotlin/github/buriedincode/kraken/schemas/Arc.kt +++ b/src/main/kotlin/github/buriedincode/kraken/schemas/Arc.kt @@ -6,6 +6,18 @@ import kotlinx.serialization.ExperimentalSerializationApi import kotlinx.serialization.Serializable import kotlinx.serialization.json.JsonNames +/** + * A data model representing an arc. + * + * @property comicvineId The Comic Vine ID of the arc. + * @property description The description of the arc. + * @property grandComicsDatabaseId The Grand Comics Database ID of the arc. + * @property id The unique identifier of the resource. + * @property image The image URL of the arc. + * @property modified The date and time when the resource was last modified. + * @property name The name of the resource. + * @property resourceUrl The URL of the arc resource. + */ @OptIn(ExperimentalSerializationApi::class) @Serializable data class Arc( diff --git a/src/main/kotlin/github/buriedincode/kraken/schemas/Character.kt b/src/main/kotlin/github/buriedincode/kraken/schemas/Character.kt index 9325152..c3c0ba0 100644 --- a/src/main/kotlin/github/buriedincode/kraken/schemas/Character.kt +++ b/src/main/kotlin/github/buriedincode/kraken/schemas/Character.kt @@ -7,6 +7,22 @@ import kotlinx.serialization.ExperimentalSerializationApi import kotlinx.serialization.Serializable import kotlinx.serialization.json.JsonNames +/** + * A data model representing a character. + * + * @property alias The aliases of the character. + * @property comicvineId The Comic Vine ID of the character. + * @property creators The creators of the character. + * @property description The description of the character. + * @property grandComicsDatabaseId The Grand Comics Database ID of the character + * @property id The unique identifier of the resource. + * @property image The image URL of the character. + * @property modified The date and time when the resource was last modified. + * @property name The name of the resource. + * @property resourceUrl The URL of the character resource. + * @property teams The teams the character belongs to. + * @property universes The universes the character is associated with. + */ @OptIn(ExperimentalSerializationApi::class) @Serializable data class Character( diff --git a/src/main/kotlin/github/buriedincode/kraken/schemas/Common.kt b/src/main/kotlin/github/buriedincode/kraken/schemas/Common.kt index b1445c7..0dd508e 100644 --- a/src/main/kotlin/github/buriedincode/kraken/schemas/Common.kt +++ b/src/main/kotlin/github/buriedincode/kraken/schemas/Common.kt @@ -4,6 +4,16 @@ import github.buriedincode.kraken.serializers.NullableStringSerializer import kotlinx.datetime.Instant import kotlinx.serialization.Serializable +/** + * A generic response wrapper for paginated API results. + * + * @param T The type of items contained in the `results` list. + * @property count The total number of items available in the resource. + * @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, @@ -14,12 +24,25 @@ data class ListResponse( val results: List = listOf(), ) +/** + * A data model representing a generic item. + * + * @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, ) +/** + * A data model representing a base resource. + * + * @property id The unique identifier of the base resource. + * @property modified The date and time when the base resource was last modified. + * @property name The name of the base resource. + */ @Serializable data class BaseResource( val id: Long, diff --git a/src/main/kotlin/github/buriedincode/kraken/schemas/Creator.kt b/src/main/kotlin/github/buriedincode/kraken/schemas/Creator.kt index 9aa96f8..06125b8 100644 --- a/src/main/kotlin/github/buriedincode/kraken/schemas/Creator.kt +++ b/src/main/kotlin/github/buriedincode/kraken/schemas/Creator.kt @@ -7,6 +7,21 @@ import kotlinx.serialization.ExperimentalSerializationApi import kotlinx.serialization.Serializable import kotlinx.serialization.json.JsonNames +/** + * A data model representing a creator. + * + * @property alias The aliases of the creator. + * @property birth The birthdate of the creator. + * @property comicvineId The Comic Vine ID of the creator. + * @property death The death date of the creator. + * @property description The description of the creator. + * @property grandComicsDatabaseId The Grand Comics Database ID of the creator. + * @property id The unique identifier of the resource. + * @property image The image URL of the creator. + * @property modified The date and time when the resource was last modified. + * @property name The name of the resource. + * @property resourceUrl The URL of the creator resource. + */ @OptIn(ExperimentalSerializationApi::class) @Serializable data class Creator( diff --git a/src/main/kotlin/github/buriedincode/kraken/schemas/Imprint.kt b/src/main/kotlin/github/buriedincode/kraken/schemas/Imprint.kt index fa19aa7..56b6a8b 100644 --- a/src/main/kotlin/github/buriedincode/kraken/schemas/Imprint.kt +++ b/src/main/kotlin/github/buriedincode/kraken/schemas/Imprint.kt @@ -6,6 +6,20 @@ import kotlinx.serialization.ExperimentalSerializationApi import kotlinx.serialization.Serializable import kotlinx.serialization.json.JsonNames +/** + * A data model representing an imprint. + * + * @property comicvineId The Comic Vine ID of the publisher. + * @property description The description of the publisher. + * @property founded The year the publisher was founded. + * @property grandComicsDatabaseId The Grand Comics Database ID of the publisher. + * @property id The unique identifier of the resource. + * @property image The image URL of the publisher. + * @property modified The date and time when the resource was last modified. + * @property name The name of the resource. + * @property publisher The generic item representing the publisher. + * @property resourceUrl The URL of the publisher resource. + */ @OptIn(ExperimentalSerializationApi::class) @Serializable data class Imprint( diff --git a/src/main/kotlin/github/buriedincode/kraken/schemas/Issue.kt b/src/main/kotlin/github/buriedincode/kraken/schemas/Issue.kt index d701252..d3d5841 100644 --- a/src/main/kotlin/github/buriedincode/kraken/schemas/Issue.kt +++ b/src/main/kotlin/github/buriedincode/kraken/schemas/Issue.kt @@ -7,6 +7,19 @@ import kotlinx.serialization.ExperimentalSerializationApi import kotlinx.serialization.Serializable import kotlinx.serialization.json.JsonNames +/** + * A data model representing a basic issue + * + * @property coverDate The cover date of the issue. + * @property coverHash The hash value of the issue cover. + * @property id The unique identifier of the issue. + * @property image The image URL of the issue. + * @property modified The date and time when the issue was last modified. + * @property number The number of the issue. + * @property series The basic series associated with the issue. + * @property storeDate The store date of the issue. + * @property title The name of the issue. + */ @OptIn(ExperimentalSerializationApi::class) @Serializable data class BasicIssue( @@ -17,12 +30,19 @@ data class BasicIssue( @Serializable(with = NullableStringSerializer::class) val image: String? = null, val modified: Instant, - @JsonNames("issue") - val name: String, 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, @@ -31,6 +51,40 @@ data class BasicIssue( ) } +/** + * A data model representing an issue. + * + * @property alternativeNumber The alternative number of the issue. + * @property arcs The arcs associated with the issue. + * @property characters The characters featured in the issue. + * @property comicvineId The Comic Vine ID of the issue. + * @property coverDate The cover date of the issue. + * @property coverHash The hash value of the issue cover. + * @property credits The credits for the issue. + * @property description The description 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. + * @property imprint The imprint of the issue or None. + * @property isbn The International Standard Book Number (ISBN) of the issue. + * @property modified The date and time when the issue was last modified. + * @property number The number of the issue. + * @property pageCount The number of pages in the issue. + * @property price The price of the issue. + * @property publisher The publisher of the issue. + * @property rating The rating of the issue. + * @property reprints The reprints of the issue. + * @property resourceUrl The URL of the issue. + * @property series The series to which the issue belongs. + * @property sku The Stock Keeping Unit (SKU) of the issue. + * @property storeDate The store date of the issue. + * @property stories The titles of the stories in the issue. + * @property teams The teams involved in the issue. + * @property title The title of the issue. + * @property universes The universes related to the issue. + * @property upc The Universal Product Code (UPC) of the issue. + * @property variants The variants of the issue. + */ @OptIn(ExperimentalSerializationApi::class) @Serializable data class Issue( @@ -79,6 +133,13 @@ data class Issue( 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, @@ -87,14 +148,30 @@ data class Issue( 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, - @JsonNames("issue") - val name: String, + 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(), @@ -106,6 +183,14 @@ data class Issue( 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, diff --git a/src/main/kotlin/github/buriedincode/kraken/schemas/Publisher.kt b/src/main/kotlin/github/buriedincode/kraken/schemas/Publisher.kt index 59d6ae3..0c35438 100644 --- a/src/main/kotlin/github/buriedincode/kraken/schemas/Publisher.kt +++ b/src/main/kotlin/github/buriedincode/kraken/schemas/Publisher.kt @@ -6,6 +6,20 @@ import kotlinx.serialization.ExperimentalSerializationApi import kotlinx.serialization.Serializable import kotlinx.serialization.json.JsonNames +/** + * A data model representing a publisher. + * + * @property comicvineId The Comic Vine ID of the publisher. + * @property country An ISO 3166-1 2-letter country code. + * @property description The description of the publisher. + * @property founded The year the publisher was founded. + * @property grandComicsDatabaseId The Grand Comics Database ID of the publisher. + * @property id The unique identifier of the resource. + * @property image The image URL of the publisher. + * @property modified The date and time when the resource was last modified. + * @property name The name of the resource. + * @property resourceUrl The URL of the publisher. + */ @OptIn(ExperimentalSerializationApi::class) @Serializable data class Publisher( diff --git a/src/main/kotlin/github/buriedincode/kraken/schemas/Series.kt b/src/main/kotlin/github/buriedincode/kraken/schemas/Series.kt index 0552d70..6a01723 100644 --- a/src/main/kotlin/github/buriedincode/kraken/schemas/Series.kt +++ b/src/main/kotlin/github/buriedincode/kraken/schemas/Series.kt @@ -6,6 +6,16 @@ import kotlinx.serialization.ExperimentalSerializationApi import kotlinx.serialization.Serializable import kotlinx.serialization.json.JsonNames +/** + * A data model representing a basic series. + * + * @property id The unique identifier of the series. + * @property issueCount The number of issues in the series. + * @property modified The date and time when the series was last modified. + * @property name The name of the series. + * @property volume The volume number of the series. + * @property yearBegan The year the series began. + */ @OptIn(ExperimentalSerializationApi::class) @Serializable data class BasicSeries( @@ -18,6 +28,28 @@ data class BasicSeries( val yearBegan: Int, ) +/** + * A data model representing a series. + * + * @property associated The associated series. + * @property comicvineId The Comic Vine ID of the series. + * @property description The description of the series. + * @property genres The genres associated with the series. + * @property grandComicsDatabaseId The Grand Comics Database ID of the series. + * @property id The unique identifier of the series. + * @property imprint The imprint of the series or None. + * @property issueCount The number of issues in the series. + * @property modified The date and time when the series was last modified. + * @property name The name of the series. + * @property publisher The publisher of the series. + * @property resourceUrl The URL of the series resource. + * @property seriesType The type of series. + * @property status The status of the 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. + * @property yearEnd The year the series ended. + */ @OptIn(ExperimentalSerializationApi::class) @Serializable data class Series( @@ -44,6 +76,12 @@ data class Series( 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( diff --git a/src/main/kotlin/github/buriedincode/kraken/schemas/Team.kt b/src/main/kotlin/github/buriedincode/kraken/schemas/Team.kt index 930ff16..6afcc47 100644 --- a/src/main/kotlin/github/buriedincode/kraken/schemas/Team.kt +++ b/src/main/kotlin/github/buriedincode/kraken/schemas/Team.kt @@ -6,6 +6,20 @@ import kotlinx.serialization.ExperimentalSerializationApi import kotlinx.serialization.Serializable import kotlinx.serialization.json.JsonNames +/** + * A data model representing a team. + * + * @property comicvineId The Comic Vine ID of the team. + * @property creators The creators of the team. + * @property description The description of the team. + * @property grandComicsDatabaseId The Grand Comics Database ID of the team. + * @property id The unique identifier of the resource. + * @property image The image URL of the team. + * @property modified The date and time when the resource was last modified. + * @property name The name of the resource. + * @property resourceUrl The URL of the team. + * @property universes The universes the team is associated with. + */ @OptIn(ExperimentalSerializationApi::class) @Serializable data class Team( diff --git a/src/main/kotlin/github/buriedincode/kraken/schemas/Universe.kt b/src/main/kotlin/github/buriedincode/kraken/schemas/Universe.kt index 5594ca5..e5a45fc 100644 --- a/src/main/kotlin/github/buriedincode/kraken/schemas/Universe.kt +++ b/src/main/kotlin/github/buriedincode/kraken/schemas/Universe.kt @@ -6,6 +6,19 @@ import kotlinx.serialization.ExperimentalSerializationApi import kotlinx.serialization.Serializable import kotlinx.serialization.json.JsonNames +/** + * A data model representing a universe. + * + * @property description The description of the universe. + * @property designation The designation fo the universe. + * @property grandComicsDatabaseId The Grand Comics Database ID of the universe. + * @property id The unique identifier of the resource. + * @property image The image URL of the universe. + * @property modified The date and time when the resource was last modified. + * @property name The name of the resource. + * @property publisher The publisher of the universe. + * @property resourceUrl The URL of the universe. + */ @OptIn(ExperimentalSerializationApi::class) @Serializable data class Universe( diff --git a/src/main/kotlin/github/buriedincode/kraken/serializers/EmptyListSerializer.kt b/src/main/kotlin/github/buriedincode/kraken/serializers/EmptyListSerializer.kt index 5377e5f..26054b1 100644 --- a/src/main/kotlin/github/buriedincode/kraken/serializers/EmptyListSerializer.kt +++ b/src/main/kotlin/github/buriedincode/kraken/serializers/EmptyListSerializer.kt @@ -8,15 +8,52 @@ import kotlinx.serialization.descriptors.listSerialDescriptor import kotlinx.serialization.encoding.Decoder 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. + * + * @param T The type of elements contained within the list. + * @property elementSerializer The serializer used for the individual elements in the list. + * + * ### Example Usage: + * ```kotlin + * @Serializable + * data class Example( + * @Serializable(with = EmptyListSerializer::class) + * val items: List + * ) + * + * val json = """{"items": null}""" + * val result = Json.decodeFromString(json) + * println(result.items) // Output: [] + * ``` + */ 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) + /** + * 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) } diff --git a/src/main/kotlin/github/buriedincode/kraken/serializers/NullableStringSerializer.kt b/src/main/kotlin/github/buriedincode/kraken/serializers/NullableStringSerializer.kt index ebfeeb3..b73b9d4 100644 --- a/src/main/kotlin/github/buriedincode/kraken/serializers/NullableStringSerializer.kt +++ b/src/main/kotlin/github/buriedincode/kraken/serializers/NullableStringSerializer.kt @@ -1,5 +1,6 @@ package github.buriedincode.kraken.serializers +import kotlinx.serialization.ExperimentalSerializationApi import kotlinx.serialization.KSerializer import kotlinx.serialization.descriptors.PrimitiveKind import kotlinx.serialization.descriptors.PrimitiveSerialDescriptor @@ -7,15 +8,56 @@ import kotlinx.serialization.descriptors.SerialDescriptor import kotlinx.serialization.encoding.Decoder 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. + * + * ### Example Usage: + * ```kotlin + * @Serializable + * data class Example( + * @Serializable(with = NullableStringSerializer::class) + * val description: String? + * ) + * + * val json = """{"description": ""}""" + * val result = Json.decodeFromString(json) + * println(result.description) // Output: null + * + * val serialized = Json.encodeToString(Example(description = null)) + * println(serialized) // Output: {"description":null} + * ``` + */ object NullableStringSerializer : KSerializer { + /** + * 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 } } + /** + * 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?) { - encoder.encodeString(value ?: "") + if (value.isNullOrBlank()) { + encoder.encodeNull() + } else { + encoder.encodeString(value) + } } } diff --git a/src/test/kotlin/github/buriedincode/kraken/schemas/IssueTest.kt b/src/test/kotlin/github/buriedincode/kraken/schemas/IssueTest.kt index 8e5a898..3124226 100644 --- a/src/test/kotlin/github/buriedincode/kraken/schemas/IssueTest.kt +++ b/src/test/kotlin/github/buriedincode/kraken/schemas/IssueTest.kt @@ -38,7 +38,7 @@ class IssueTest { { 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].name) }, + { assertEquals("Bone (1991) #1", results[0].title) }, { assertEquals("1", results[0].number) }, { assertAll( @@ -111,7 +111,7 @@ class IssueTest { { assertAll( { assertEquals(113595, result.reprints[0].id) }, - { assertEquals("Bone TPB (2004) #1", result.reprints[0].name) }, + { assertEquals("Bone TPB (2004) #1", result.reprints[0].issue) }, ) }, { assertEquals("https://metron.cloud/issue/bone-1991-1/", result.resourceUrl) },