Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

0.2.0: Type-safe battles, Brawlify Stats API support, bug fixes #9

Merged
merged 5 commits into from
Jan 2, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
Loading