Skip to content

Commit

Permalink
0.2.0: Type-safe battles, Brawlify Stats API support, bug fixes (#9)
Browse files Browse the repository at this point in the history
* fix (closes #4): `createOr` now uses CreationFailure in its lambda

* feat: type-safe battles (closes #8)

* feat: add support for showdown matches (closes #6), add support for friendly matches (closes #5), add support for map-maker (closes #7)

* fix: unnecessary macos system usage

* feat: added ability to change the base url of brawl stars API client
  • Loading branch information
y9vad9 authored Jan 2, 2025
1 parent 1d796f4 commit 78718f3
Show file tree
Hide file tree
Showing 44 changed files with 2,846 additions and 177 deletions.
2 changes: 1 addition & 1 deletion .github/workflows/publish.yml
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ on:

jobs:
build:
runs-on: macos-latest
runs-on: ubuntu-latest

steps:
- name: Checkout repository
Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/run-tests.yml
Original file line number Diff line number Diff line change
Expand Up @@ -17,5 +17,5 @@ jobs:
distribution: 'corretto'
java-version: '11'
cache: 'gradle'
# Library current does not support testing official API client automatically
# Library current does not support testing an official API client automatically
- run: ./gradlew jvmTest --no-daemon
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import com.y9vad9.brawlifyapi.types.icons.BrawlifyClubIcon
import com.y9vad9.brawlifyapi.types.icons.BrawlifyPlayerIcon
import com.y9vad9.brawlifyapi.types.maps.BrawlifyMap
import com.y9vad9.bsapi.types.event.value.EventId
import com.y9vad9.bsapi.types.event.value.isPublic
import io.ktor.client.*
import io.ktor.client.call.*
import io.ktor.client.engine.*
Expand Down Expand Up @@ -62,6 +63,10 @@ public class BrawlifyClient(
* [API Documentation](https://brawlapi.com/#/endpoints/maps)
*/
public suspend fun getMap(eventId: EventId): Result<BrawlifyMap?> = runCatching {
// fast-way: no way to get the event, it's most likely to be
// map-maker
if (!eventId.isPublic) return@runCatching null

val result = client.get("maps/${eventId.raw}")

when (result.status) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@ package com.y9vad9.brawlifyapi.types.events
import com.y9vad9.brawlifyapi.types.events.value.BrawlifyEventEmoji
import com.y9vad9.brawlifyapi.types.events.value.BrawlifyEventName
import com.y9vad9.brawlifyapi.types.maps.BrawlifyMap
import com.y9vad9.bsapi.types.event.value.EventId
import com.y9vad9.bsapi.types.event.value.EventSlotId
import kotlinx.datetime.Instant
import kotlinx.serialization.Serializable
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@ import com.y9vad9.brawlifyapi.types.events.BrawlifyGameMode
import com.y9vad9.brawlifyapi.types.events.value.BrawlifyUrl
import com.y9vad9.brawlifyapi.types.maps.value.BrawlifyEnvironmentId
import com.y9vad9.brawlifyapi.types.maps.value.BrawlifyEnvironmentName
import com.y9vad9.brawlifyapi.types.stats.BrawlifyBrawlerStat
import com.y9vad9.brawlifyapi.types.stats.BrawlifyTeamStat
import com.y9vad9.bsapi.types.event.value.EventId
import com.y9vad9.bsapi.types.event.value.MapName
import com.y9vad9.bsapi.types.player.value.PlayerName
Expand Down Expand Up @@ -36,6 +38,8 @@ public data class BrawlifyMap(
val dataUpdated: Instant,
@Serializable(with = InstantFromUnixMillisecondsSerializer::class)
val lastActive: Instant?,
val stats: List<BrawlifyBrawlerStat> = emptyList(),
val teamStats: List<BrawlifyTeamStat> = emptyList(),
) {
@Serializable
public data class Environment(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,6 @@ public data class BrawlifyBrawlerStat(
val brawler: BrawlerId,
val winRate: BrawlifyRate,
val useRate: BrawlifyRate,
// star player rate
val starRate: BrawlifyRate,
// star player rate (for teams battles)
val starRate: BrawlifyRate? = null,
)
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
package com.y9vad9.brawlifyapi.types.stats

import com.y9vad9.brawlifyapi.types.common.value.BrawlifyHash
import com.y9vad9.brawlifyapi.types.common.value.BrawlifyRate
import com.y9vad9.bsapi.types.brawler.value.BrawlerId
import com.y9vad9.bsapi.types.common.value.Count
import kotlinx.serialization.Serializable
import kotlinx.serialization.Transient

@Serializable
public data class BrawlifyTeamStat(
val name: BrawlifyBrawlerStat,
val hash: BrawlifyHash,
val brawler1: BrawlerId,
val brawler2: BrawlerId,
val brawler3: BrawlerId? = null,
val brawler4: BrawlerId? = null,
val brawler5: BrawlerId? = null,
val data: Data,
) {
@Transient
public val brawlers: List<BrawlerId> = listOfNotNull(brawler1, brawler2, brawler3, brawler4, brawler5)

@Serializable
public data class Data(
val winRate: BrawlifyRate,
val useRate: BrawlifyRate,
val wins: Count,
val losses: Count,
val draws: Count,
val total: Count,
)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
package com.y9vad9.brawlifyapi.types.stats.value

import com.y9vad9.bsapi.types.ValueConstructor
import com.y9vad9.bsapi.types.exception.CreationFailure
import kotlinx.serialization.Serializable
import kotlin.jvm.JvmInline

@Serializable
@JvmInline
public value class BrawlifyTeamName private constructor(public val raw: String) {
public companion object : ValueConstructor<BrawlifyTeamName, String> {
override fun create(value: String): Result<BrawlifyTeamName> {
if (value.isBlank()) return Result.failure(CreationFailure.ofBlank())
return Result.success(BrawlifyTeamName(value))
}
}
}
1 change: 1 addition & 0 deletions brawlify/src/jvmTest/kotlin/BrawlifyClientTest.kt
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ class BrawlifyClientIntegrationTest {
assertTrue(result.isSuccess, "Expected getEvents() to succeed but it failed.")
val events = result.getOrNull()
assertNotNull(events, "Expected non-null response from getEvents().")

println("Active events: ${events.active.size}, Upcoming events: ${events.upcoming.size}")
}

Expand Down
34 changes: 23 additions & 11 deletions core/src/commonMain/kotlin/com/y9vad9/bsapi/BrawlStarsClient.kt
Original file line number Diff line number Diff line change
Expand Up @@ -7,14 +7,15 @@ import com.y9vad9.bsapi.types.club.ClubMember
import com.y9vad9.bsapi.types.club.value.ClubTag
import com.y9vad9.bsapi.types.common.value.Count
import com.y9vad9.bsapi.types.common.value.CountryCode
import com.y9vad9.bsapi.types.event.Battle
import com.y9vad9.bsapi.types.event.battle.RawBattle
import com.y9vad9.bsapi.types.event.ScheduledEvent
import com.y9vad9.bsapi.types.exception.ClientError
import com.y9vad9.bsapi.types.exception.BrawlStarsAPIException
import com.y9vad9.bsapi.types.pagination.Cursors
import com.y9vad9.bsapi.types.pagination.Page
import com.y9vad9.bsapi.types.pagination.PagesIterator
import com.y9vad9.bsapi.types.player.Player
import com.y9vad9.bsapi.types.player.value.PlayerTag
import com.y9vad9.bsapi.types.player.value.withHashTag
import io.ktor.client.*
import io.ktor.client.call.*
import io.ktor.client.engine.*
Expand Down Expand Up @@ -42,11 +43,12 @@ public class BrawlStarsClient(
bearerToken: String,
json: Json = Json { ignoreUnknownKeys = true },
engine: HttpClientEngine,
baseUrl: String = "https://api.brawlstars.com/v1/",
configBlock: HttpClientConfig<*>.() -> Unit = {},
) {
private val client: HttpClient = HttpClient(engine) {
defaultRequest {
url("https://api.brawlstars.com/v1/")
url(baseUrl)
accept(ContentType.Application.Json)

bearerAuth(bearerToken)
Expand All @@ -66,20 +68,20 @@ public class BrawlStarsClient(
* @return [Result] containing the [Player] object if successful, or null if the player was not found.
*/
public suspend fun getPlayer(tag: PlayerTag): Result<Player?> =
getRequest(typeInfo<Player>(), "players/${tag.toString().replace("#", "%23")}")
getRequest(typeInfo<Player>(), "players/${tag.withHashTag.replace("#", "%23")}")

/**
* Retrieves a player's battle log, showing recent battles.
*
* **Note:** New battles may take up to 30 minutes to appear in the battle log.
*
* @param tag The unique player tag (e.g., #PLAYER_TAG).
* @return [Result] containing a list of [Battle] objects if successful, or null if the player was not found.
* @return [Result] containing a list of [RawBattle] objects if successful, or null if the player was not found.
*/
public suspend fun getPlayerBattlelog(tag: PlayerTag): Result<List<Battle>?> =
getRequest<ItemsResponse<Battle>>(
typeInfo<ItemsResponse<Battle>>(),
"players/${tag.toString().replace("#", "%23")}/battlelog"
public suspend fun getPlayerBattlelog(tag: PlayerTag): Result<List<RawBattle>?> =
getRequest<ItemsResponse<RawBattle>>(
typeInfo<ItemsResponse<RawBattle>>(),
"players/${tag.withHashTag.replace("#", "%23")}/battlelog"
).map { it?.items }

/**
Expand Down Expand Up @@ -199,7 +201,7 @@ public class BrawlStarsClient(
return getRequest<ItemsResponse<Player.Ranking>>(
typeInfo = typeInfo<ItemsResponse<Player.Ranking>>(),
url = "rankings/${countryCode.value}/players",
).map { it!!.items }
).map { it?.items.orEmpty() }
}

/**
Expand Down Expand Up @@ -308,8 +310,18 @@ public class BrawlStarsClient(
result.body<T>(typeInfo)
} else if (result.status == HttpStatusCode.NotFound) {
null
} else if (result.status == HttpStatusCode.BadRequest) {
throw BrawlStarsAPIException.BadRequest()
} else if (result.status == HttpStatusCode.Forbidden) {
throw BrawlStarsAPIException.AccessDenied()
} else if(result.status == HttpStatusCode.TooManyRequests) {
throw BrawlStarsAPIException.LimitsExceeded()
} else if (result.status == HttpStatusCode.InternalServerError) {
throw BrawlStarsAPIException.InternalServerError()
} else if (result.status == HttpStatusCode.ServiceUnavailable) {
throw BrawlStarsAPIException.UnderMaintenance()
} else {
throw result.body<ClientError>()
throw BrawlStarsAPIException.RawHttpError(result.status)
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,4 +5,4 @@ package com.y9vad9.bsapi.annotations
message = "This declaration works only within special conditions, please refer to the documentation.",
level = RequiresOptIn.Level.WARNING,
)
public annotation class ContextualApi
public annotation class ContextualBSApi
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
package com.y9vad9.bsapi.internal

import com.y9vad9.bsapi.types.ValueConstructor
import com.y9vad9.bsapi.types.createUnsafe
import com.y9vad9.bsapi.types.player.value.BotTag
import com.y9vad9.bsapi.types.player.value.EntityTag
import com.y9vad9.bsapi.types.player.value.PlayerTag
import com.y9vad9.bsapi.types.player.value.withHashTag
import kotlinx.serialization.KSerializer
import kotlinx.serialization.descriptors.PrimitiveKind
import kotlinx.serialization.descriptors.PrimitiveSerialDescriptor
import kotlinx.serialization.descriptors.SerialDescriptor
import kotlinx.serialization.encoding.Decoder
import kotlinx.serialization.encoding.Encoder

internal object EntityTagSerializer : KSerializer<EntityTag> {
override val descriptor: SerialDescriptor =
PrimitiveSerialDescriptor("EntityTag", PrimitiveKind.STRING)

@OptIn(ValueConstructor.Unsafe::class)
override fun deserialize(decoder: Decoder): EntityTag {
val value = decoder.decodeString().replace("#", "")

return when (value.length) {
// bot's tags are 3 symbols (or four if with hashtag)
3 -> BotTag.createUnsafe(value)
else -> PlayerTag.createUnsafe(value)
}
}

override fun serialize(encoder: Encoder, value: EntityTag) {
encoder.encodeString(value.withHashTag)
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -31,9 +31,11 @@ public interface ValueConstructor<Type, WrappedType> {

public inline fun <T, W> ValueConstructor<T, W>.createOr(
value: W,
otherwise: (Throwable) -> T,
otherwise: (CreationFailure) -> T,
): T {
return create(value).getOrElse(otherwise)
return create(value).getOrElse {
otherwise(it as CreationFailure)
}
}

public fun <T, W> ValueConstructor<T, W>.createOrNull(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,11 @@ import com.y9vad9.bsapi.types.brawler.value.BrawlerName
import com.y9vad9.bsapi.types.brawler.value.BrawlerRank
import com.y9vad9.bsapi.types.brawler.value.PowerLevel
import com.y9vad9.bsapi.types.club.Club
import com.y9vad9.bsapi.types.event.value.RankingPosition
import com.y9vad9.bsapi.types.event.value.Trophies
import com.y9vad9.bsapi.types.player.PlayerIcon
import com.y9vad9.bsapi.types.player.value.PlayerName
import com.y9vad9.bsapi.types.player.value.PlayerTag
import kotlinx.serialization.Serializable


Expand All @@ -32,11 +35,12 @@ public data class Brawler(

@Serializable
public data class Ranking(
val id: BrawlerId,
val name: BrawlerName,
val name: PlayerName,
val tag: PlayerTag,
val icon: PlayerIcon,
val trophies: Trophies,
val club: Club.View,
val club: Club.View? = null,
val rank: RankingPosition,
)
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,12 +5,27 @@ import com.y9vad9.bsapi.types.exception.CreationFailure
import kotlinx.serialization.Serializable
import kotlin.jvm.JvmInline

/**
* Power-level of the brawler. Might be negative if it's a friendly match.
*/
@Serializable
@JvmInline
public value class PowerLevel private constructor(private val value: Int) : Comparable<PowerLevel> {
public companion object : ValueConstructor<PowerLevel, Int> {
// we accept 12 for forward-compatibility as such rumors going on in the community
public val VALUE_RANGE: IntRange = 1..12
public val VALUE_RANGE: IntRange = -1..11

/**
* Undefined power level occurs in the friendly matches.
*/
public val UNDEFINED: PowerLevel = PowerLevel(-1)

/**
* Normal minimum of the [PowerLevel] level. Don't use
* for validation for the input of battle log – if it's a friendly match
* it will be [UNDEFINED] of value `-1`.
*/
public val MIN: PowerLevel = PowerLevel(1)
public val MAX: PowerLevel = PowerLevel(11)

override fun create(value: Int): Result<PowerLevel> {
if (value !in VALUE_RANGE) return Result.failure(CreationFailure.ofRange(VALUE_RANGE))
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ public data class Club(

@Serializable
public data class Ranking(
val clubTag: ClubTag,
val tag: ClubTag,
val name: ClubName,
val trophies: Trophies,
val rank: RankingPosition,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ public value class CountryCode private constructor(public val value: String) {
/**
* Used to provide information about global instead of localized information.
*/
public val GLOBAL: CountryCode = CountryCode("GLOBAL")
public val GLOBAL: CountryCode = CountryCode("global")

public val UKRAINE: CountryCode = CountryCode("UA")
public val GERMANY: CountryCode = CountryCode("DE")
Expand Down
Loading

0 comments on commit 78718f3

Please sign in to comment.