From 981c02cd9691ddc959db98f437d962672febfd43 Mon Sep 17 00:00:00 2001 From: noahkohrs Date: Wed, 11 Sep 2024 17:25:59 +0200 Subject: [PATCH 1/4] refactor: beginning the transition to a separated module static api. --- build.gradle.kts | 2 + gradle.properties | 2 +- riot-api/build.gradle.kts | 7 +- riot-static/build.gradle.kts | 28 ++++ .../riot/staticapi/ApiClientFactory.kt | 17 +++ .../com/noahkohrs/riot/staticapi/Dto.kt | 135 ++++++++++++++++++ .../noahkohrs/riot/staticapi/RiotStaticApi.kt | 16 +++ .../riot/staticapi/lol/LoLStaticApi.kt | 43 ++++++ .../riot/staticapi/lol/LoLStaticApiTest.kt | 39 +++++ settings.gradle.kts | 3 +- 10 files changed, 286 insertions(+), 6 deletions(-) create mode 100644 riot-static/build.gradle.kts create mode 100644 riot-static/src/main/kotlin/com/noahkohrs/riot/staticapi/ApiClientFactory.kt create mode 100644 riot-static/src/main/kotlin/com/noahkohrs/riot/staticapi/Dto.kt create mode 100644 riot-static/src/main/kotlin/com/noahkohrs/riot/staticapi/RiotStaticApi.kt create mode 100644 riot-static/src/main/kotlin/com/noahkohrs/riot/staticapi/lol/LoLStaticApi.kt create mode 100644 riot-static/src/test/kotlin/com/noahkohrs/riot/staticapi/lol/LoLStaticApiTest.kt diff --git a/build.gradle.kts b/build.gradle.kts index ba78aec..41827b0 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -8,3 +8,5 @@ subprojects { apply(plugin = "org.jlleitschuh.gradle.ktlint") } +group = "com.noahkohrs" +version = "1.0-SNAPSHOT" \ No newline at end of file diff --git a/gradle.properties b/gradle.properties index 7fc6f1f..29e08e8 100644 --- a/gradle.properties +++ b/gradle.properties @@ -1 +1 @@ -kotlin.code.style=official +kotlin.code.style=official \ No newline at end of file diff --git a/riot-api/build.gradle.kts b/riot-api/build.gradle.kts index 5512587..6d0fa51 100644 --- a/riot-api/build.gradle.kts +++ b/riot-api/build.gradle.kts @@ -5,15 +5,14 @@ plugins { kotlin("jvm") } -group = "com.noahkohrs" -version = "1.0-SNAPSHOT" +group = rootProject.group +version = rootProject.version dependencies { - // Feign - // implementation("io.github.openfeign:feign-core:13.3") implementation("io.github.openfeign:feign-moshi:13.3") implementation("com.squareup.moshi:moshi-kotlin:1.15.1") + project(":riot-static") testImplementation(kotlin("test")) } diff --git a/riot-static/build.gradle.kts b/riot-static/build.gradle.kts new file mode 100644 index 0000000..69d5184 --- /dev/null +++ b/riot-static/build.gradle.kts @@ -0,0 +1,28 @@ +import org.jetbrains.kotlin.gradle.dsl.ExplicitApiMode + + +apply(plugin = "org.jetbrains.dokka") + +plugins { + kotlin("jvm") +} + +group = rootProject.group +version = rootProject.version + +dependencies { + implementation("io.github.openfeign:feign-moshi:13.3") + implementation("com.squareup.moshi:moshi-kotlin:1.15.1") + + testImplementation(kotlin("test")) +} + +tasks.test { + useJUnitPlatform() +} + +kotlin { + jvmToolchain(21) + explicitApi() + explicitApi = ExplicitApiMode.Strict +} diff --git a/riot-static/src/main/kotlin/com/noahkohrs/riot/staticapi/ApiClientFactory.kt b/riot-static/src/main/kotlin/com/noahkohrs/riot/staticapi/ApiClientFactory.kt new file mode 100644 index 0000000..c125f9f --- /dev/null +++ b/riot-static/src/main/kotlin/com/noahkohrs/riot/staticapi/ApiClientFactory.kt @@ -0,0 +1,17 @@ +package com.noahkohrs.riot.staticapi + +import com.squareup.moshi.Moshi +import com.squareup.moshi.kotlin.reflect.KotlinJsonAdapterFactory +import feign.Feign +import feign.moshi.MoshiDecoder +import feign.moshi.MoshiEncoder + +internal object StaticRiotApiFactory { + fun create(clazz: Class, baseUrl: String): T { + val moshi = Moshi.Builder().add(KotlinJsonAdapterFactory()).build() + return Feign.builder() + .decoder(MoshiDecoder(moshi)) + .encoder(MoshiEncoder(moshi)) + .target(clazz, baseUrl) + } +} diff --git a/riot-static/src/main/kotlin/com/noahkohrs/riot/staticapi/Dto.kt b/riot-static/src/main/kotlin/com/noahkohrs/riot/staticapi/Dto.kt new file mode 100644 index 0000000..2f820ef --- /dev/null +++ b/riot-static/src/main/kotlin/com/noahkohrs/riot/staticapi/Dto.kt @@ -0,0 +1,135 @@ +package com.noahkohrs.riot.staticapi + +import com.squareup.moshi.Json + +internal data class ResponseWrapper( + @Json(name = "data") + val data: T, +) + +// TODO: Consider if the static Api should work with a separation between responses and dtos as the normal api. +public data class ChampionDto( +// "version": "14.18.1", +// "id": "Aatrox", + @Json(name = "id") + val id: String, +// "key": "266", + @Json(name = "key") + val key: String, +// "name": "Aatrox", + @Json(name = "name") + val name: String, +// "title": "the Darkin Blade", + @Json(name = "title") + val title: String, +// "blurb": "Once honored defenders of Shurima against the Void, Aatrox and his brethren would eventually become an even greater threat to Runeterra, and were defeated only by cunning mortal sorcery. But after centuries of imprisonment, Aatrox was the first to find...", + @Json(name = "blurb") + val blurb: String, +// "info": { +// "attack": 8, +// "defense": 4, +// "magic": 3, +// "difficulty": 4 +// }, + @Json(name = "info") + val info: Info, + // TODO: Consider how to handle the image field + // Might be nice to give access to solid object which is able to render the img. +// "image": { +// "full": "Aatrox.png", +// "sprite": "champion0.png", +// "group": "champion", +// "x": 0, +// "y": 0, +// "w": 48, +// "h": 48 +// }, +// "tags": [ +// "Fighter" +// ], + // TODO: Consider rewriting this to return enumeration field + @Json(name = "tags") + val tags: List, +// "partype": "Blood Well", + @Json(name = "partype") + val partype: String, +// "stats": { +// "hp": 650, +// "hpperlevel": 114, +// "mp": 0, +// "mpperlevel": 0, +// "movespeed": 345, +// "armor": 38, +// "armorperlevel": 4.8, +// "spellblock": 32, +// "spellblockperlevel": 2.05, +// "attackrange": 175, +// "hpregen": 3, +// "hpregenperlevel": 0.5, +// "mpregen": 0, +// "mpregenperlevel": 0, +// "crit": 0, +// "critperlevel": 0, +// "attackdamage": 60, +// "attackdamageperlevel": 5, +// "attackspeedperlevel": 2.5, +// "attackspeed": 0.651 +// } + @Json(name = "stats") + val stats: Stats, +) { + public data class Info( + @Json(name = "attack") + val attack: Int, + @Json(name = "defense") + val defense: Int, + @Json(name = "magic") + val magic: Int, + @Json(name = "difficulty") + val difficulty: Int, + ) + + // TODO: Make the naming more readable + public data class Stats( + @Json(name = "hp") + val hp: Int, + @Json(name = "hpperlevel") + val hpperlevel: Int, + @Json(name = "mp") + val mp: Int, + @Json(name = "mpperlevel") + val mpperlevel: Float, + @Json(name = "movespeed") + val movespeed: Int, + @Json(name = "armor") + val armor: Int, + @Json(name = "armorperlevel") + val armorperlevel: Float, + @Json(name = "spellblock") + val spellblock: Int, + @Json(name = "spellblockperlevel") + val spellblockperlevel: Float, + @Json(name = "attackrange") + val attackrange: Int, + @Json(name = "hpregen") + val hpregen: Float, + @Json(name = "hpregenperlevel") + val hpregenperlevel: Float, + @Json(name = "mpregen") + val mpregen: Float, + @Json(name = "mpregenperlevel") + val mpregenperlevel: Float, + @Json(name = "crit") + val crit: Int, + @Json(name = "critperlevel") + val critperlevel: Int, + @Json(name = "attackdamage") + val attackdamage: Int, + @Json(name = "attackdamageperlevel") + val attackdamageperlevel: Float, + @Json(name = "attackspeedperlevel") + val attackspeedperlevel: Float, + @Json(name = "attackspeed") + val attackspeed: Float, + ) +} diff --git a/riot-static/src/main/kotlin/com/noahkohrs/riot/staticapi/RiotStaticApi.kt b/riot-static/src/main/kotlin/com/noahkohrs/riot/staticapi/RiotStaticApi.kt new file mode 100644 index 0000000..ebb0e62 --- /dev/null +++ b/riot-static/src/main/kotlin/com/noahkohrs/riot/staticapi/RiotStaticApi.kt @@ -0,0 +1,16 @@ +package com.noahkohrs.riot.staticapi + +import com.noahkohrs.riot.staticapi.lol.LoLStaticApi + +public class RiotStaticApi { + /* + * Valorant Static Api + * - https://dash.valorant-api.com + * + * LoL Static Api + * - https://static.developer.riotgames.com/docs/lol + * - https://ddragon.leagueoflegends.com + */ + @JvmField + public val lol: LoLStaticApi = LoLStaticApi() +} diff --git a/riot-static/src/main/kotlin/com/noahkohrs/riot/staticapi/lol/LoLStaticApi.kt b/riot-static/src/main/kotlin/com/noahkohrs/riot/staticapi/lol/LoLStaticApi.kt new file mode 100644 index 0000000..7c07131 --- /dev/null +++ b/riot-static/src/main/kotlin/com/noahkohrs/riot/staticapi/lol/LoLStaticApi.kt @@ -0,0 +1,43 @@ +package com.noahkohrs.riot.staticapi.lol + +import com.noahkohrs.riot.staticapi.ChampionDto +import com.noahkohrs.riot.staticapi.ResponseWrapper +import com.noahkohrs.riot.staticapi.StaticRiotApiFactory +import feign.RequestLine + +private const val VERSION = "14.18.1" +private const val LANG = "en_US" + +public class LoLStaticApi { + private val apiClient = StaticRiotApiFactory.create(LoLStaticApiClient::class.java, "https://ddragon.leagueoflegends.com") + + /** + * Get versions. + */ + public fun getVersions(): List = apiClient.getVersions() + + /** + * Get latest version. + */ + public fun getLatestVersion(): String = getVersions().first() + + // TODO: Consider rewriting this to return enumeration field. Consider languages conflict between different LoL Apis + public fun getLanguages(): Set = apiClient.getLanguages().toSet() + + /** + * Get champions. + */ + public fun getChampions(): List = apiClient.getChampions().data.values.toList() + + internal interface LoLStaticApiClient { + @RequestLine("GET /api/versions.json") + fun getVersions(): List + + // https://ddragon.leagueoflegends.com/cdn/languages.json + @RequestLine("GET /cdn/languages.json") + fun getLanguages(): List + + @RequestLine("GET /cdn/$VERSION/data/$LANG/champion.json") + fun getChampions(): ResponseWrapper> + } +} diff --git a/riot-static/src/test/kotlin/com/noahkohrs/riot/staticapi/lol/LoLStaticApiTest.kt b/riot-static/src/test/kotlin/com/noahkohrs/riot/staticapi/lol/LoLStaticApiTest.kt new file mode 100644 index 0000000..5a27f86 --- /dev/null +++ b/riot-static/src/test/kotlin/com/noahkohrs/riot/staticapi/lol/LoLStaticApiTest.kt @@ -0,0 +1,39 @@ +package com.noahkohrs.riot.staticapi.lol + +import com.noahkohrs.riot.staticapi.RiotStaticApi +import org.junit.jupiter.api.Assertions.* +import org.junit.jupiter.api.Test + +class LoLStaticApiTest { + val api = RiotStaticApi().lol + + @Test + fun getVersions() { + val versions = api.getVersions() + assertTrue(versions.isNotEmpty()) + } + + @Test + fun getLatestVersion() { + val version = api.getLatestVersion() + assertTrue(version.isNotEmpty()) + } + + @Test + fun getLanguages() { + val languages = api.getLanguages() + assertTrue(languages.isNotEmpty()) + } + + @Test + fun getChampions() { + val champions = api.getChampions() + assertTrue(champions.isNotEmpty()) + + champions.forEach { + assertTrue(it.id.isNotEmpty()) + assertTrue(it.name.isNotEmpty()) + assertTrue(it.title.isNotEmpty()) + } + } +} diff --git a/settings.gradle.kts b/settings.gradle.kts index d588313..189f22d 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -13,4 +13,5 @@ dependencyResolutionManagement { repositories { mavenCentral() } -} \ No newline at end of file +} +include("riot-static") From 18a63a83734f5de6d9e3c9a21277d2520c2d5339 Mon Sep 17 00:00:00 2001 From: noahkohrs Date: Fri, 13 Sep 2024 19:55:37 +0200 Subject: [PATCH 2/4] feat: adding champion + skinsplash getters --- .../riot/staticapi/ApiClientFactory.kt | 15 +- .../com/noahkohrs/riot/staticapi/Dto.kt | 243 ++++++++++-------- .../riot/staticapi/lol/LoLStaticApi.kt | 38 ++- .../riot/staticapi/lol/LoLStaticApiTest.kt | 15 +- settings.gradle.kts | 2 +- 5 files changed, 192 insertions(+), 121 deletions(-) diff --git a/riot-static/src/main/kotlin/com/noahkohrs/riot/staticapi/ApiClientFactory.kt b/riot-static/src/main/kotlin/com/noahkohrs/riot/staticapi/ApiClientFactory.kt index c125f9f..36c9615 100644 --- a/riot-static/src/main/kotlin/com/noahkohrs/riot/staticapi/ApiClientFactory.kt +++ b/riot-static/src/main/kotlin/com/noahkohrs/riot/staticapi/ApiClientFactory.kt @@ -5,13 +5,26 @@ import com.squareup.moshi.kotlin.reflect.KotlinJsonAdapterFactory import feign.Feign import feign.moshi.MoshiDecoder import feign.moshi.MoshiEncoder +import java.lang.reflect.Type internal object StaticRiotApiFactory { - fun create(clazz: Class, baseUrl: String): T { + fun createJsonClient(clazz: Class, baseUrl: String): T { val moshi = Moshi.Builder().add(KotlinJsonAdapterFactory()).build() return Feign.builder() .decoder(MoshiDecoder(moshi)) .encoder(MoshiEncoder(moshi)) .target(clazz, baseUrl) } + + fun createImageClient(clazz: Class, baseUrl: String): T { + return Feign.builder() + .decoder(ByteArrayDecoder()) + .target(clazz, baseUrl) + } +} + +internal class ByteArrayDecoder : feign.codec.Decoder { + override fun decode(response: feign.Response, type: Type): Any { + return response.body().asInputStream().readBytes() + } } diff --git a/riot-static/src/main/kotlin/com/noahkohrs/riot/staticapi/Dto.kt b/riot-static/src/main/kotlin/com/noahkohrs/riot/staticapi/Dto.kt index 2f820ef..0e74735 100644 --- a/riot-static/src/main/kotlin/com/noahkohrs/riot/staticapi/Dto.kt +++ b/riot-static/src/main/kotlin/com/noahkohrs/riot/staticapi/Dto.kt @@ -8,128 +8,145 @@ internal data class ResponseWrapper( ) // TODO: Consider if the static Api should work with a separation between responses and dtos as the normal api. -public data class ChampionDto( -// "version": "14.18.1", -// "id": "Aatrox", +public open class ChampionSummaryDto( @Json(name = "id") - val id: String, -// "key": "266", + public val id: String, @Json(name = "key") - val key: String, -// "name": "Aatrox", + public val key: String, @Json(name = "name") - val name: String, -// "title": "the Darkin Blade", + public val name: String, @Json(name = "title") - val title: String, -// "blurb": "Once honored defenders of Shurima against the Void, Aatrox and his brethren would eventually become an even greater threat to Runeterra, and were defeated only by cunning mortal sorcery. But after centuries of imprisonment, Aatrox was the first to find...", + public val title: String, @Json(name = "blurb") - val blurb: String, -// "info": { -// "attack": 8, -// "defense": 4, -// "magic": 3, -// "difficulty": 4 -// }, + public val blurb: String, @Json(name = "info") - val info: Info, - // TODO: Consider how to handle the image field - // Might be nice to give access to solid object which is able to render the img. -// "image": { -// "full": "Aatrox.png", -// "sprite": "champion0.png", -// "group": "champion", -// "x": 0, -// "y": 0, -// "w": 48, -// "h": 48 -// }, -// "tags": [ -// "Fighter" -// ], + public val info: Info, + @Json(name = "image") + public val image: Image, // TODO: Consider rewriting this to return enumeration field @Json(name = "tags") - val tags: List, -// "partype": "Blood Well", + public val tags: List, @Json(name = "partype") - val partype: String, -// "stats": { -// "hp": 650, -// "hpperlevel": 114, -// "mp": 0, -// "mpperlevel": 0, -// "movespeed": 345, -// "armor": 38, -// "armorperlevel": 4.8, -// "spellblock": 32, -// "spellblockperlevel": 2.05, -// "attackrange": 175, -// "hpregen": 3, -// "hpregenperlevel": 0.5, -// "mpregen": 0, -// "mpregenperlevel": 0, -// "crit": 0, -// "critperlevel": 0, -// "attackdamage": 60, -// "attackdamageperlevel": 5, -// "attackspeedperlevel": 2.5, -// "attackspeed": 0.651 -// } + public val partype: String, @Json(name = "stats") - val stats: Stats, -) { - public data class Info( - @Json(name = "attack") - val attack: Int, - @Json(name = "defense") - val defense: Int, - @Json(name = "magic") - val magic: Int, - @Json(name = "difficulty") - val difficulty: Int, - ) + public val stats: Stats, +) - // TODO: Make the naming more readable - public data class Stats( - @Json(name = "hp") - val hp: Int, - @Json(name = "hpperlevel") - val hpperlevel: Int, - @Json(name = "mp") - val mp: Int, - @Json(name = "mpperlevel") - val mpperlevel: Float, - @Json(name = "movespeed") - val movespeed: Int, - @Json(name = "armor") - val armor: Int, - @Json(name = "armorperlevel") - val armorperlevel: Float, - @Json(name = "spellblock") - val spellblock: Int, - @Json(name = "spellblockperlevel") - val spellblockperlevel: Float, - @Json(name = "attackrange") - val attackrange: Int, - @Json(name = "hpregen") - val hpregen: Float, - @Json(name = "hpregenperlevel") - val hpregenperlevel: Float, - @Json(name = "mpregen") - val mpregen: Float, - @Json(name = "mpregenperlevel") - val mpregenperlevel: Float, - @Json(name = "crit") - val crit: Int, - @Json(name = "critperlevel") - val critperlevel: Int, - @Json(name = "attackdamage") - val attackdamage: Int, - @Json(name = "attackdamageperlevel") - val attackdamageperlevel: Float, - @Json(name = "attackspeedperlevel") - val attackspeedperlevel: Float, - @Json(name = "attackspeed") - val attackspeed: Float, +public class ChampionDto( + id: String, + key: String, + name: String, + title: String, + blurb: String, + info: Info, + image: Image, + @Json(name = "skins") public val skins: List, + @Json(name = "lore") public val lore: String, + @Json(name = "allytips") public val allyTips: List, + @Json(name = "enemytips") public val enemyTips: List, + tags: List, + partype: String, + stats: Stats, + @Json(name = "spells") public val spells: List, + @Json(name = "passive") public val passive: Passive, +) : ChampionSummaryDto( + id = id, + key = key, + name = name, + title = title, + blurb = blurb, + info = info, + image = image, + tags = tags, + partype = partype, + stats = stats, ) -} + +public data class Stats( + @Json(name = "hp") + val hp: Int, + @Json(name = "hpperlevel") + val hpperlevel: Int, + @Json(name = "mp") + val mp: Int, + @Json(name = "mpperlevel") + val mpperlevel: Float, + @Json(name = "movespeed") + val movespeed: Int, + @Json(name = "armor") + val armor: Int, + @Json(name = "armorperlevel") + val armorperlevel: Float, + @Json(name = "spellblock") + val spellblock: Int, + @Json(name = "spellblockperlevel") + val spellblockperlevel: Float, + @Json(name = "attackrange") + val attackrange: Int, + @Json(name = "hpregen") + val hpregen: Float, + @Json(name = "hpregenperlevel") + val hpregenperlevel: Float, + @Json(name = "mpregen") + val mpregen: Float, + @Json(name = "mpregenperlevel") + val mpregenperlevel: Float, + @Json(name = "crit") + val crit: Int, + @Json(name = "critperlevel") + val critperlevel: Int, + @Json(name = "attackdamage") + val attackdamage: Int, + @Json(name = "attackdamageperlevel") + val attackdamageperlevel: Float, + @Json(name = "attackspeedperlevel") + val attackspeedperlevel: Float, + @Json(name = "attackspeed") + val attackspeed: Float, +) + +public data class Info( + @Json(name = "attack") val attack: Int, + @Json(name = "defense") val defense: Int, + @Json(name = "magic") val magic: Int, + @Json(name = "difficulty") val difficulty: Int, +) + +public data class Image( + @Json(name = "full") val full: String, + @Json(name = "sprite") val sprite: String, + @Json(name = "group") val group: String, + @Json(name = "x") val x: Int, + @Json(name = "y") val y: Int, + @Json(name = "w") val width: Int, + @Json(name = "h") val height: Int, +) + +public data class Skin( + @Json(name = "id") val id: String, + @Json(name = "num") val num: Int, + @Json(name = "name") val name: String, + @Json(name = "chromas") val chromas: Boolean, +) + +public data class Spell( + @Json(name = "id") val id: String, + @Json(name = "name") val name: String, + @Json(name = "description") val description: String, + @Json(name = "tooltip") val tooltip: String, + @Json(name = "maxrank") val maxRank: Int, + @Json(name = "cooldown") val cooldown: List, + @Json(name = "cooldownBurn") val cooldownBurn: String, + @Json(name = "cost") val cost: List, + @Json(name = "costBurn") val costBurn: String, + @Json(name = "range") val range: List, + @Json(name = "rangeBurn") val rangeBurn: String, + @Json(name = "image") val image: Image, + @Json(name = "resource") val resource: String, +) + +public data class Passive( + @Json(name = "name") val name: String, + @Json(name = "description") val description: String, + @Json(name = "image") val image: Image, +) diff --git a/riot-static/src/main/kotlin/com/noahkohrs/riot/staticapi/lol/LoLStaticApi.kt b/riot-static/src/main/kotlin/com/noahkohrs/riot/staticapi/lol/LoLStaticApi.kt index 7c07131..0bdd36b 100644 --- a/riot-static/src/main/kotlin/com/noahkohrs/riot/staticapi/lol/LoLStaticApi.kt +++ b/riot-static/src/main/kotlin/com/noahkohrs/riot/staticapi/lol/LoLStaticApi.kt @@ -1,20 +1,27 @@ package com.noahkohrs.riot.staticapi.lol import com.noahkohrs.riot.staticapi.ChampionDto +import com.noahkohrs.riot.staticapi.ChampionSummaryDto import com.noahkohrs.riot.staticapi.ResponseWrapper import com.noahkohrs.riot.staticapi.StaticRiotApiFactory +import feign.Param import feign.RequestLine private const val VERSION = "14.18.1" private const val LANG = "en_US" public class LoLStaticApi { - private val apiClient = StaticRiotApiFactory.create(LoLStaticApiClient::class.java, "https://ddragon.leagueoflegends.com") + private val ddragonClient = StaticRiotApiFactory.createJsonClient(LoLStaticApiClient::class.java, "https://ddragon.leagueoflegends.com") + private val ddragonImgClient = + StaticRiotApiFactory.createImageClient( + LoLStaticApiClient::class.java, + "https://ddragon.leagueoflegends.com", + ) /** * Get versions. */ - public fun getVersions(): List = apiClient.getVersions() + public fun getVersions(): List = ddragonClient.getVersions() /** * Get latest version. @@ -22,12 +29,22 @@ public class LoLStaticApi { public fun getLatestVersion(): String = getVersions().first() // TODO: Consider rewriting this to return enumeration field. Consider languages conflict between different LoL Apis - public fun getLanguages(): Set = apiClient.getLanguages().toSet() + public fun getLanguages(): Set = ddragonClient.getLanguages().toSet() /** * Get champions. */ - public fun getChampions(): List = apiClient.getChampions().data.values.toList() + public fun getChampions(): List = ddragonClient.getChampions().data.values.toList() + + /** + * Get champion. + */ + public fun getChampion(championName: String): ChampionDto = ddragonClient.getChampion(championName).data.values.first() + + /** + * Get champion splash. + */ + public fun getChampionSplash(championName: String, skinNum: Int): ByteArray = ddragonImgClient.getChampionSplash(championName, skinNum) internal interface LoLStaticApiClient { @RequestLine("GET /api/versions.json") @@ -38,6 +55,17 @@ public class LoLStaticApi { fun getLanguages(): List @RequestLine("GET /cdn/$VERSION/data/$LANG/champion.json") - fun getChampions(): ResponseWrapper> + fun getChampions(): ResponseWrapper> + + @RequestLine("GET /cdn/$VERSION/data/$LANG/champion/{championName}.json") + fun getChampion( + @Param("championName") championName: String, + ): ResponseWrapper> + + @RequestLine("GET /cdn/img/champion/splash/{championName}_{skinNum}.jpg") + fun getChampionSplash( + @Param("championName") championName: String, + @Param("skinNum") skinNum: Int, + ): ByteArray } } diff --git a/riot-static/src/test/kotlin/com/noahkohrs/riot/staticapi/lol/LoLStaticApiTest.kt b/riot-static/src/test/kotlin/com/noahkohrs/riot/staticapi/lol/LoLStaticApiTest.kt index 5a27f86..854bd0c 100644 --- a/riot-static/src/test/kotlin/com/noahkohrs/riot/staticapi/lol/LoLStaticApiTest.kt +++ b/riot-static/src/test/kotlin/com/noahkohrs/riot/staticapi/lol/LoLStaticApiTest.kt @@ -29,11 +29,24 @@ class LoLStaticApiTest { fun getChampions() { val champions = api.getChampions() assertTrue(champions.isNotEmpty()) - + println(champions[0]) champions.forEach { assertTrue(it.id.isNotEmpty()) assertTrue(it.name.isNotEmpty()) assertTrue(it.title.isNotEmpty()) } } + + @Test + fun getChampion() { + val champion = api.getChampion("Aatrox") + assertNotNull(champion) + println(champion) + } + + @Test + fun getChampionSplash() { + val splash = api.getChampionSplash("Aatrox", 0) + assertTrue(splash.isNotEmpty()) + } } diff --git a/settings.gradle.kts b/settings.gradle.kts index 189f22d..61a80d6 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -4,6 +4,7 @@ plugins { rootProject.name = "riot-api-sdk" include("riot-api") include("test-apps") +include("riot-static") // Repositories @@ -14,4 +15,3 @@ dependencyResolutionManagement { mavenCentral() } } -include("riot-static") From 14f3f1e264c2a402eb518c3ce4d03c9143d5e524 Mon Sep 17 00:00:00 2001 From: noahkohrs Date: Tue, 24 Sep 2024 14:25:07 +0200 Subject: [PATCH 3/4] refactor: removing static api from the main module --- .../statics/ObjectReps.kt | 156 ------------------ .../statics/StaticsRiotData.kt | 26 --- .../noahkohrs/riot/api/StaticsRiotDataTest.kt | 20 --- 3 files changed, 202 deletions(-) delete mode 100644 riot-api/src/main/kotlin/com.noahkohrs.riot.api/statics/ObjectReps.kt delete mode 100644 riot-api/src/main/kotlin/com.noahkohrs.riot.api/statics/StaticsRiotData.kt delete mode 100644 riot-api/src/test/kotlin/com/noahkohrs/riot/api/StaticsRiotDataTest.kt diff --git a/riot-api/src/main/kotlin/com.noahkohrs.riot.api/statics/ObjectReps.kt b/riot-api/src/main/kotlin/com.noahkohrs.riot.api/statics/ObjectReps.kt deleted file mode 100644 index 3ccad0c..0000000 --- a/riot-api/src/main/kotlin/com.noahkohrs.riot.api/statics/ObjectReps.kt +++ /dev/null @@ -1,156 +0,0 @@ -package com.noahkohrs.riot.api.statics - -import com.noahkohrs.riot.api.statics.StaticsRiotData.queues - -public data class LoLQueue( - val queueId: Int, - val map: String, - val description: String?, - val notes: String?, -) { - public companion object { - public fun fromId(queueId: Int): LoLQueue { - return queues[queueId] ?: error("Queue $queueId not found") - } - } -} - -public data class LoLMap( - val mapId: Int, - val mapName: String, - val notes: String, -) - -internal data class ItemDto( - val data: Map, -) - -public data class LoLItem( -// "name": "", - val name: String, -// "rune": { - val rune: Rune, -// "gold": { - val gold: Gold, -// "group": "", - val group: String, -// "description": "", - val description: String, -// "colloq": ";", - val colloq: String, -// "plaintext": "", - val plaintext: String, -// "consumed": false, - val consumed: Boolean, -// "stacks": 1, - val stacks: Int, -// "depth": 1, - val depth: Int, -// "consumeOnFull": false, - val consumeOnFull: Boolean, -// "from": [], - val from: List, -// "into": [], - val into: List, -// "specialRecipe": 0, - val specialRecipe: Int, -// "inStore": true, - val inStore: Boolean, -// "hideFromAll": false, - val hideFromAll: Boolean, -// "requiredChampion": "", - val requiredChampion: String, -// "requiredAlly": "", - val requiredAlly: String, -// "stats": { - val stats: Map, -// "FlatHPPoolMod": 0, -// "rFlatHPModPerLevel": 0, -// "FlatMPPoolMod": 0, -// "rFlatMPModPerLevel": 0, -// "PercentHPPoolMod": 0, -// "PercentMPPoolMod": 0, -// "FlatHPRegenMod": 0, -// "rFlatHPRegenModPerLevel": 0, -// "PercentHPRegenMod": 0, -// "FlatMPRegenMod": 0, -// "rFlatMPRegenModPerLevel": 0, -// "PercentMPRegenMod": 0, -// "FlatArmorMod": 0, -// "rFlatArmorModPerLevel": 0, -// "PercentArmorMod": 0, -// "rFlatArmorPenetrationMod": 0, -// "rFlatArmorPenetrationModPerLevel": 0, -// "rPercentArmorPenetrationMod": 0, -// "rPercentArmorPenetrationModPerLevel": 0, -// "FlatPhysicalDamageMod": 0, -// "rFlatPhysicalDamageModPerLevel": 0, -// "PercentPhysicalDamageMod": 0, -// "FlatMagicDamageMod": 0, -// "rFlatMagicDamageModPerLevel": 0, -// "PercentMagicDamageMod": 0, -// "FlatMovementSpeedMod": 0, -// "rFlatMovementSpeedModPerLevel": 0, -// "PercentMovementSpeedMod": 0, -// "rPercentMovementSpeedModPerLevel": 0, -// "FlatAttackSpeedMod": 0, -// "PercentAttackSpeedMod": 0, -// "rPercentAttackSpeedModPerLevel": 0, -// "rFlatDodgeMod": 0, -// "rFlatDodgeModPerLevel": 0, -// "PercentDodgeMod": 0, -// "FlatCritChanceMod": 0, -// "rFlatCritChanceModPerLevel": 0, -// "PercentCritChanceMod": 0, -// "FlatCritDamageMod": 0, -// "rFlatCritDamageModPerLevel": 0, -// "PercentCritDamageMod": 0, -// "FlatBlockMod": 0, -// "PercentBlockMod": 0, -// "FlatSpellBlockMod": 0, -// "rFlatSpellBlockModPerLevel": 0, -// "PercentSpellBlockMod": 0, -// "FlatEXPBonus": 0, -// "PercentEXPBonus": 0, -// "rPercentCooldownMod": 0, -// "rPercentCooldownModPerLevel": 0, -// "rFlatTimeDeadMod": 0, -// "rFlatTimeDeadModPerLevel": 0, -// "rPercentTimeDeadMod": 0, -// "rPercentTimeDeadModPerLevel": 0, -// "rFlatGoldPer10Mod": 0, -// "rFlatMagicPenetrationMod": 0, -// "rFlatMagicPenetrationModPerLevel": 0, -// "rPercentMagicPenetrationMod": 0, -// "rPercentMagicPenetrationModPerLevel": 0, -// "FlatEnergyRegenMod": 0, -// "rFlatEnergyRegenModPerLevel": 0, -// "FlatEnergyPoolMod": 0, -// "rFlatEnergyModPerLevel": 0, -// "PercentLifeStealMod": 0, -// "PercentSpellVampMod": 0 -// }, -// "tags": [], - val tags: List, -// "maps": { -// "1": true, -// "8": true, -// "10": true, -// "12": true -// } - val maps: Map, -// }, -) { - public data class Rune( - val isrune: Boolean, - val tier: Int, - val type: String, - ) - - public data class Gold( - val base: Int, - val total: Int, - val sell: Int, - val purchasable: Boolean, - ) -} diff --git a/riot-api/src/main/kotlin/com.noahkohrs.riot.api/statics/StaticsRiotData.kt b/riot-api/src/main/kotlin/com.noahkohrs.riot.api/statics/StaticsRiotData.kt deleted file mode 100644 index 42418f9..0000000 --- a/riot-api/src/main/kotlin/com.noahkohrs.riot.api/statics/StaticsRiotData.kt +++ /dev/null @@ -1,26 +0,0 @@ -package com.noahkohrs.riot.api.statics - -import com.noahkohrs.riot.api.StaticRiotDragoonFactory -import feign.RequestLine - -internal object StaticsRiotData { - private val staticApi = - StaticRiotDragoonFactory.create() - val queues = staticApi.getQueues().associateBy { it.queueId } - - val maps = staticApi.getMaps().associateBy { it.mapId } - - // TODO: NOT WORKING YET - // val items = staticApi.getItems().data - - internal interface StaticDragoonApi { - @RequestLine("GET /docs/lol/queues.json") - fun getQueues(): List - - @RequestLine("GET /docs/lol/maps.json") - fun getMaps(): List - - @RequestLine("GET /docs/lol/item.json") - fun getItems(): ItemDto - } -} diff --git a/riot-api/src/test/kotlin/com/noahkohrs/riot/api/StaticsRiotDataTest.kt b/riot-api/src/test/kotlin/com/noahkohrs/riot/api/StaticsRiotDataTest.kt deleted file mode 100644 index 173459a..0000000 --- a/riot-api/src/test/kotlin/com/noahkohrs/riot/api/StaticsRiotDataTest.kt +++ /dev/null @@ -1,20 +0,0 @@ -package com.noahkohrs.riot.api - -import com.noahkohrs.riot.api.statics.StaticsRiotData -import org.junit.jupiter.api.Assertions.* -import kotlin.test.Test - -class StaticsRiotDataTest { - @Test - fun getQueues() { - val currentQueuesNumber = 96 - - assertEquals(StaticsRiotData.queues.size, currentQueuesNumber) - } - - @Test - fun getMaps() { - val currentMapsNumber = 16 - assertEquals(StaticsRiotData.maps.size, currentMapsNumber) - } -} From 720b66023dc5a618e82408420d53f713b5287bac Mon Sep 17 00:00:00 2001 From: noahkohrs Date: Tue, 24 Sep 2024 14:25:40 +0200 Subject: [PATCH 4/4] feat: adding basics static endpoints --- .../ApiClientFactory.kt | 18 ----- .../riot/staticapi/ApiClientFactory.kt | 13 +++- .../riot/staticapi/lol/LoLStaticApi.kt | 72 ++++++++++++++++--- .../noahkohrs/riot/staticapi/values/Images.kt | 44 ++++++++++++ .../riot/staticapi/lol/LoLStaticApiTest.kt | 10 ++- 5 files changed, 123 insertions(+), 34 deletions(-) create mode 100644 riot-static/src/main/kotlin/com/noahkohrs/riot/staticapi/values/Images.kt diff --git a/riot-api/src/main/kotlin/com.noahkohrs.riot.api/ApiClientFactory.kt b/riot-api/src/main/kotlin/com.noahkohrs.riot.api/ApiClientFactory.kt index c17c562..eba3a38 100644 --- a/riot-api/src/main/kotlin/com.noahkohrs.riot.api/ApiClientFactory.kt +++ b/riot-api/src/main/kotlin/com.noahkohrs.riot.api/ApiClientFactory.kt @@ -1,7 +1,6 @@ package com.noahkohrs.riot.api import com.noahkohrs.riot.api.manipulation.UnpredictableDtoAdapterFactory -import com.noahkohrs.riot.api.statics.StaticsRiotData import com.noahkohrs.riot.api.values.AccountRegion import com.noahkohrs.riot.api.values.GlobalRegion import com.noahkohrs.riot.api.values.Platform @@ -76,23 +75,6 @@ internal object GlobalRegionApiClientFactory { } } -/** - * Planned to be replaced by an entire new module for the project. - * - * Do not expend. - */ -internal object StaticRiotDragoonFactory { - private const val STATIC_API_URL = "https://static.developer.riotgames.com" - - fun create(): StaticsRiotData.StaticDragoonApi { - val moshi = Moshi.Builder().add(KotlinJsonAdapterFactory()).build() - return Feign.builder() - .decoder(MoshiDecoder(moshi)) - .encoder(MoshiEncoder(moshi)) - .target(StaticsRiotData.StaticDragoonApi::class.java, STATIC_API_URL) - } -} - internal class RiotApiErrorDecoder : ErrorDecoder { private val defaultDecoder = ErrorDecoder.Default() diff --git a/riot-static/src/main/kotlin/com/noahkohrs/riot/staticapi/ApiClientFactory.kt b/riot-static/src/main/kotlin/com/noahkohrs/riot/staticapi/ApiClientFactory.kt index 36c9615..2877723 100644 --- a/riot-static/src/main/kotlin/com/noahkohrs/riot/staticapi/ApiClientFactory.kt +++ b/riot-static/src/main/kotlin/com/noahkohrs/riot/staticapi/ApiClientFactory.kt @@ -1,5 +1,6 @@ package com.noahkohrs.riot.staticapi +import com.noahkohrs.riot.staticapi.values.* import com.squareup.moshi.Moshi import com.squareup.moshi.kotlin.reflect.KotlinJsonAdapterFactory import feign.Feign @@ -18,13 +19,19 @@ internal object StaticRiotApiFactory { fun createImageClient(clazz: Class, baseUrl: String): T { return Feign.builder() - .decoder(ByteArrayDecoder()) + .decoder(ImageDecoder()) .target(clazz, baseUrl) } } -internal class ByteArrayDecoder : feign.codec.Decoder { +internal class ImageDecoder : feign.codec.Decoder { override fun decode(response: feign.Response, type: Type): Any { - return response.body().asInputStream().readBytes() + val bytes = response.body().asInputStream().readBytes() + response.body().asInputStream().close() + return Image( + url = response.request().url(), + format = ImageFormat.parseFormat(response.request().url()), + bytes = bytes, + ) } } diff --git a/riot-static/src/main/kotlin/com/noahkohrs/riot/staticapi/lol/LoLStaticApi.kt b/riot-static/src/main/kotlin/com/noahkohrs/riot/staticapi/lol/LoLStaticApi.kt index 0bdd36b..abdf292 100644 --- a/riot-static/src/main/kotlin/com/noahkohrs/riot/staticapi/lol/LoLStaticApi.kt +++ b/riot-static/src/main/kotlin/com/noahkohrs/riot/staticapi/lol/LoLStaticApi.kt @@ -4,14 +4,31 @@ import com.noahkohrs.riot.staticapi.ChampionDto import com.noahkohrs.riot.staticapi.ChampionSummaryDto import com.noahkohrs.riot.staticapi.ResponseWrapper import com.noahkohrs.riot.staticapi.StaticRiotApiFactory +import com.noahkohrs.riot.staticapi.values.Image import feign.Param import feign.RequestLine private const val VERSION = "14.18.1" private const val LANG = "en_US" -public class LoLStaticApi { +public class LoLStaticApi( + private val version: String = VERSION, + private val lang: String = LANG, +) { private val ddragonClient = StaticRiotApiFactory.createJsonClient(LoLStaticApiClient::class.java, "https://ddragon.leagueoflegends.com") + + private val ddragonDynamicClient = + StaticRiotApiFactory.createJsonClient( + LoLStaticDynamicApiClient::class.java, + "https://ddragon.leagueoflegends.com/cdn/$version/data/$lang/", + ) + + private val ddragonPatchRelative = + StaticRiotApiFactory.createImageClient( + LoLStaticApiClient::class.java, + "https://ddragon.leagueoflegends.com/cdn/$version", + ) + private val ddragonImgClient = StaticRiotApiFactory.createImageClient( LoLStaticApiClient::class.java, @@ -34,17 +51,27 @@ public class LoLStaticApi { /** * Get champions. */ - public fun getChampions(): List = ddragonClient.getChampions().data.values.toList() + public fun getChampions(): List = ddragonDynamicClient.getChampions().data.values.toList() /** * Get champion. */ - public fun getChampion(championName: String): ChampionDto = ddragonClient.getChampion(championName).data.values.first() + public fun getChampion(championName: String): ChampionDto = ddragonDynamicClient.getChampion(championName).data.values.first() /** * Get champion splash. */ - public fun getChampionSplash(championName: String, skinNum: Int): ByteArray = ddragonImgClient.getChampionSplash(championName, skinNum) + public fun getChampionSplash(championName: String, skinNum: Int): Image = ddragonImgClient.getChampionSplash(championName, skinNum) + + /** + * Get passive. + */ + public fun getPassive(name: String): Image = ddragonPatchRelative.getPassive(name) + + /** + * Get spell. + */ + public fun getSpell(name: String): Image = ddragonPatchRelative.getSpell(name) internal interface LoLStaticApiClient { @RequestLine("GET /api/versions.json") @@ -54,18 +81,41 @@ public class LoLStaticApi { @RequestLine("GET /cdn/languages.json") fun getLanguages(): List - @RequestLine("GET /cdn/$VERSION/data/$LANG/champion.json") + @RequestLine("GET /cdn/img/champion/splash/{championName}_{skinNum}.jpg") + fun getChampionSplash( + @Param("championName") championName: String, + @Param("skinNum") skinNum: Int, + ): Image + + @RequestLine("GET /img/passive/{img}") + fun getPassive( + @Param("img") img: String, + ): Image + + @RequestLine("GET /img/spell/{img}") + fun getSpell( + @Param("img") name: String, + ): Image + + @RequestLine("GET /img/item/{img}") + fun getItem( + /** + * Item ID +.png + */ + @Param("name") img: String, + ): Image + } + + internal interface LoLStaticDynamicApiClient { + @RequestLine("GET /champion.json") fun getChampions(): ResponseWrapper> - @RequestLine("GET /cdn/$VERSION/data/$LANG/champion/{championName}.json") + @RequestLine("GET /champion/{championName}.json") fun getChampion( @Param("championName") championName: String, ): ResponseWrapper> - @RequestLine("GET /cdn/img/champion/splash/{championName}_{skinNum}.jpg") - fun getChampionSplash( - @Param("championName") championName: String, - @Param("skinNum") skinNum: Int, - ): ByteArray + @RequestLine("GET /item.json") + fun getItems(): ResponseWrapper> } } diff --git a/riot-static/src/main/kotlin/com/noahkohrs/riot/staticapi/values/Images.kt b/riot-static/src/main/kotlin/com/noahkohrs/riot/staticapi/values/Images.kt new file mode 100644 index 0000000..36fba31 --- /dev/null +++ b/riot-static/src/main/kotlin/com/noahkohrs/riot/staticapi/values/Images.kt @@ -0,0 +1,44 @@ +package com.noahkohrs.riot.staticapi.values + +public enum class ImageFormat { + PNG, + JPG, + ; + + public companion object { + public fun parseFormat(url: String): ImageFormat { + val fileExt = url.substringAfterLast('.').substringBefore('?') + return when (fileExt) { + "png" -> PNG + "jpg" -> JPG + else -> throw IllegalArgumentException("Unknown image format: $url") + } + } + } +} + +public data class Image( + val url: String, + val format: ImageFormat, + val bytes: ByteArray, +) { + override fun equals(other: Any?): Boolean { + if (this === other) return true + if (javaClass != other?.javaClass) return false + + other as Image + + if (url != other.url) return false + if (format != other.format) return false + if (!bytes.contentEquals(other.bytes)) return false + + return true + } + + override fun hashCode(): Int { + var result = url.hashCode() + result = 31 * result + format.hashCode() + result = 31 * result + bytes.contentHashCode() + return result + } +} diff --git a/riot-static/src/test/kotlin/com/noahkohrs/riot/staticapi/lol/LoLStaticApiTest.kt b/riot-static/src/test/kotlin/com/noahkohrs/riot/staticapi/lol/LoLStaticApiTest.kt index 854bd0c..8f4ceaf 100644 --- a/riot-static/src/test/kotlin/com/noahkohrs/riot/staticapi/lol/LoLStaticApiTest.kt +++ b/riot-static/src/test/kotlin/com/noahkohrs/riot/staticapi/lol/LoLStaticApiTest.kt @@ -45,8 +45,14 @@ class LoLStaticApiTest { } @Test - fun getChampionSplash() { + fun getImages() { val splash = api.getChampionSplash("Aatrox", 0) - assertTrue(splash.isNotEmpty()) + assertTrue(splash.bytes.isNotEmpty()) + + val spell = api.getSpell("FlashFrost.png") + assertTrue(spell.bytes.isNotEmpty()) + + val passive = api.getPassive("Anivia_P.png") + assertTrue(passive.bytes.isNotEmpty()) } }