feature: init kotlin compose multiplatforme
Tests / build (pull_request) Successful in 7m44s
Tests / test (pull_request) Failing after 10m37s
Tests / lint (pull_request) Successful in 14m30s

This commit is contained in:
2026-08-06 21:28:57 +02:00
parent 505cfe38f0
commit 774e80d9d5
196 changed files with 1297 additions and 688 deletions
+46
View File
@@ -0,0 +1,46 @@
val kotlinSerializationVersion: Provider<String> = providers.gradleProperty("kotlin_serialization_version")
val androidCompileSdk: Provider<String> = providers.gradleProperty("android_compile_sdk")
val androidMinSdk: Provider<String> = providers.gradleProperty("android_min_sdk")
plugins {
kotlin("multiplatform")
kotlin("plugin.serialization")
id("com.android.library")
}
kotlin {
jvm()
androidTarget {
compilerOptions {
freeCompilerArgs.add("-opt-in=kotlin.uuid.ExperimentalUuidApi")
}
}
@OptIn(org.jetbrains.kotlin.gradle.ExperimentalWasmDsl::class)
wasmJs {
browser()
}
compilerOptions {
freeCompilerArgs.add("-opt-in=kotlin.uuid.ExperimentalUuidApi")
}
sourceSets {
commonMain.dependencies {
implementation("org.jetbrains.kotlinx:kotlinx-serialization-json:${kotlinSerializationVersion.get()}")
}
commonTest.dependencies {
implementation(kotlin("test"))
}
}
}
android {
namespace = "io.github.flecomte.eventdemo.shared"
compileSdk = androidCompileSdk.get().toInt()
defaultConfig {
minSdk = androidMinSdk.get().toInt()
}
}
@@ -0,0 +1,17 @@
package eventDemo.shared.command
import eventDemo.shared.ids.CommandId
/**
* Interface to represent a Command.
*
* A command is a request for an action.
*
* Moved to `shared` (deviating slightly from the original plan of leaving it backend-only)
* because [eventDemo.shared.game.command.GameCommand] - a wire type that must live in `shared`
* for multiplatform client reuse - implements it. Since `shared` cannot depend on `backend`,
* this minimal marker interface has to live wherever its implementers live.
*/
interface Command {
val id: CommandId
}
@@ -0,0 +1,132 @@
package eventDemo.shared.game
import eventDemo.shared.serializers.UUIDSerializer
import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable
import kotlin.uuid.Uuid
/**
* A Play card
*/
@Serializable
sealed interface Card {
val id: Uuid
/**
* The color of a card
*/
@Serializable
enum class Color {
Blue,
Red,
Yellow,
Green,
}
sealed interface CardWithColor : Card {
val color: Color
}
/**
* A play card with color and number
*/
@Serializable
@SerialName("Simple")
data class NumericCard(
val number: Int,
override val color: Color,
@Serializable(with = UUIDSerializer::class)
override val id: Uuid = Uuid.random(),
) : Card,
CardWithColor {
init {
if (number > 9) error("Card number cannot be greater of 9")
if (number < 0) error("Card number cannot be lower of 0")
}
override fun toString(): String =
"Numeric Card $number $color"
}
sealed interface Special : Card
/**
* A revert card to revert the order of the turn.
*/
@Serializable
@SerialName("Reverse")
data class ReverseCard(
override val color: Color,
@Serializable(with = UUIDSerializer::class)
override val id: Uuid = Uuid.random(),
) : Special,
CardWithColor {
override fun toString(): String =
"Revert Card $color"
}
sealed interface PassTurnCard : Card
/**
* A pass card to pass the turn of the next player.
*/
@Serializable
@SerialName("Pass")
data class PassCard(
override val color: Color,
@Serializable(with = UUIDSerializer::class)
override val id: Uuid = Uuid.random(),
) : Special,
CardWithColor,
PassTurnCard {
override fun toString(): String =
"Pass Card $color"
}
/**
* A play card to force the next player to take 2 card and pass the turn.
*/
@Serializable
@SerialName("Plus2")
data class Plus2Card(
override val color: Color,
@Serializable(with = UUIDSerializer::class)
override val id: Uuid = Uuid.random(),
) : Special,
CardWithColor,
PassTurnCard {
override fun toString(): String =
"Plus2 Card $color"
}
sealed interface CardWith4Color : Card
/**
* A play card to force the next player to take 4 card and pass the turn.
*/
@Serializable
@SerialName("Plus4")
class Plus4Card(
@Serializable(with = UUIDSerializer::class)
override val id: Uuid = Uuid.random(),
) : Special,
CardWith4Color,
PassTurnCard {
override fun toString(): String =
"Plus4 Card"
}
/**
* A play card to change the color.
*/
@Serializable
@SerialName("ChangeColor")
class ChangeColorCard(
@Serializable(with = UUIDSerializer::class)
override val id: Uuid = Uuid.random(),
) : Special,
CardWith4Color {
override fun toString(): String =
"Change color Card"
}
}
@@ -0,0 +1,19 @@
package eventDemo.shared.game
import kotlin.jvm.JvmInline
import kotlinx.serialization.Serializable
@JvmInline
@Serializable
value class DiscardPile(
val cards: Set<Card> = emptySet(),
) {
fun withNewCard(card: Card): DiscardPile =
DiscardPile(cards + card)
val topCard: Card? get() = cards.lastOrNull()
val topCardColor: Card.Color? get() = topCard?.let { if (it is Card.CardWithColor) it.color else null }
val size: Int get() = cards.size
}
@@ -0,0 +1,34 @@
package eventDemo.shared.game
import kotlin.jvm.JvmInline
import kotlinx.serialization.Serializable
@JvmInline
@Serializable
value class DrawPile(
val cards: Set<Card> = emptySet(),
) {
val size: Int get() = cards.size
fun take(number: Int): Pair<DrawPile, Set<Card>> =
cards.drop(number).toDrawPile() to cards.take(number).toSet()
val remainingCards
get() = cards.size
fun shuffled(): DrawPile =
cards.shuffled().toDrawPile()
val firstCard get() = cards.first()
fun generateValidDrawPile(): DrawPile =
if (cards.first() is Card.CardWith4Color) {
DrawPile(setOf(cards.first()) + cards.drop(1))
.generateValidDrawPile()
} else {
this
}
}
private fun List<Card>.toDrawPile(): DrawPile =
DrawPile(this.toSet())
@@ -0,0 +1,74 @@
package eventDemo.shared.game
import eventDemo.shared.ids.AggregateId
import eventDemo.shared.ids.UserId
import eventDemo.shared.serializers.UUIDSerializer
import kotlin.jvm.JvmInline
import kotlinx.serialization.Serializable
import kotlin.uuid.Uuid
@Serializable
data class Player(
val name: String,
val userId: UserId,
val hand: PlayerHand = PlayerHand(),
val id: PlayerId = PlayerId(Uuid.random()),
) {
@JvmInline
@Serializable
value class PlayerId(
@Serializable(with = UUIDSerializer::class)
override val id: Uuid = Uuid.random(),
) : AggregateId {
override fun toString(): String =
id.toString()
}
}
@Serializable
class PlayerList(
val players: Set<Player> = emptySet(),
) : Set<Player> by players {
fun get(id: Player.PlayerId): Player =
find { it.id == id } ?: error("no player with id $id")
fun get(id: UserId): Player =
find { it.userId == id } ?: error("no player with userId $id")
fun withNewCardOnPlayerHand(
playerId: Player.PlayerId,
cards: Set<Card>,
): PlayerList =
players
.withReplacedValue(get(playerId)) {
it.copy(
hand = it.hand.withNewCards(cards),
)
}.let { PlayerList(it) }
fun withDropCardOnPlayerHand(
playerId: Player.PlayerId,
card: Card,
): PlayerList =
players
.withReplacedValue(get(playerId)) {
it.copy(
hand = it.hand.withoutTheCards(setOf(card)),
)
}.let { PlayerList(it) }
operator fun plus(player: Player): PlayerList =
PlayerList(players + player)
}
private inline fun <V> Set<V>.withReplacedValue(
toReplace: V,
transform: (V) -> V,
): Set<V> =
map {
if (it == toReplace) {
transform(it)
} else {
it
}
}.toSet()
@@ -0,0 +1,19 @@
package eventDemo.shared.game
import kotlin.jvm.JvmInline
import kotlinx.serialization.Serializable
@Serializable
@JvmInline
value class PlayerHand(
val cards: Set<Card> = emptySet(),
) {
fun withNewCards(newCards: Set<Card>): PlayerHand =
PlayerHand(cards + newCards)
fun withoutTheCards(newCards: Set<Card>): PlayerHand =
PlayerHand(cards - newCards)
val size: Int get() =
cards.size
}
@@ -0,0 +1,19 @@
package eventDemo.shared.game.command
import eventDemo.shared.command.Command
import eventDemo.shared.ids.GameId
import eventDemo.shared.ids.UserId
import eventDemo.shared.serializers.GameIdSerializer
import kotlinx.serialization.Serializable
@Serializable
sealed interface GameCommand : Command {
val userId: UserId
val payload: Payload
@Serializable
sealed interface Payload {
@Serializable(with = GameIdSerializer::class)
val aggregateId: GameId
}
}
@@ -0,0 +1,24 @@
package eventDemo.shared.game.command
import eventDemo.shared.ids.CommandId
import eventDemo.shared.ids.GameId
import eventDemo.shared.ids.UserId
import eventDemo.shared.serializers.GameIdSerializer
import kotlinx.serialization.Serializable
/**
* A command to perform an action to play a new card
*/
@Serializable
data class JoinTheGameCommand(
override val userId: UserId,
override val payload: Payload,
) : GameCommand {
override val id: CommandId = CommandId()
@Serializable
data class Payload(
@Serializable(with = GameIdSerializer::class)
override val aggregateId: GameId,
) : GameCommand.Payload
}
@@ -0,0 +1,31 @@
package eventDemo.shared.game.command
import eventDemo.shared.game.Card
import eventDemo.shared.game.Player
import eventDemo.shared.ids.CommandId
import eventDemo.shared.ids.GameId
import eventDemo.shared.ids.UserId
import eventDemo.shared.serializers.GameIdSerializer
import eventDemo.shared.serializers.PlayerIdSerializer
import kotlinx.serialization.Serializable
/**
* A command to perform an action to play a new card
*/
@Serializable
data class PlayCardCommand(
override val userId: UserId,
override val payload: Payload,
) : GameCommand {
override val id: CommandId = CommandId()
@Serializable
data class Payload(
@Serializable(with = GameIdSerializer::class)
override val aggregateId: GameId,
@Serializable(with = PlayerIdSerializer::class)
val playerId: Player.PlayerId,
val card: Card,
val chosenColor: Card.Color?,
) : GameCommand.Payload
}
@@ -0,0 +1,28 @@
package eventDemo.shared.game.command
import eventDemo.shared.game.Player
import eventDemo.shared.ids.CommandId
import eventDemo.shared.ids.GameId
import eventDemo.shared.ids.UserId
import eventDemo.shared.serializers.GameIdSerializer
import eventDemo.shared.serializers.PlayerIdSerializer
import kotlinx.serialization.Serializable
/**
* A command to set as ready to play
*/
@Serializable
data class ReadyToPlayCommand(
override val userId: UserId,
override val payload: Payload,
) : GameCommand {
override val id: CommandId = CommandId()
@Serializable
data class Payload(
@Serializable(with = GameIdSerializer::class)
override val aggregateId: GameId,
@Serializable(with = PlayerIdSerializer::class)
val playerId: Player.PlayerId,
) : GameCommand.Payload
}
@@ -0,0 +1,30 @@
package eventDemo.shared.game.command
import eventDemo.shared.game.Player
import eventDemo.shared.ids.CommandId
import eventDemo.shared.ids.GameId
import eventDemo.shared.ids.UserId
import eventDemo.shared.serializers.GameIdSerializer
import eventDemo.shared.serializers.PlayerIdSerializer
import kotlinx.serialization.Serializable
/**
* A command to draw card on draw pile.
*
* Is can be triggered when you cannot play any card in your hand.
*/
@Serializable
data class TakeCartFromDrawPileCommand(
override val userId: UserId,
override val payload: Payload,
) : GameCommand {
override val id: CommandId = CommandId()
@Serializable
data class Payload(
@Serializable(with = GameIdSerializer::class)
override val aggregateId: GameId,
@Serializable(with = PlayerIdSerializer::class)
val playerId: Player.PlayerId,
) : GameCommand.Payload
}
@@ -0,0 +1,13 @@
package eventDemo.shared.game.notification
import eventDemo.shared.game.Player
import eventDemo.shared.serializers.UUIDSerializer
import kotlinx.serialization.Serializable
import kotlin.uuid.Uuid
@Serializable
data class ItsTheTurnOfNotification(
@Serializable(with = UUIDSerializer::class)
override val id: Uuid = Uuid.random(),
val player: Player,
) : Notification
@@ -0,0 +1,11 @@
package eventDemo.shared.game.notification
import eventDemo.shared.serializers.UUIDSerializer
import kotlinx.serialization.Serializable
import kotlin.uuid.Uuid
@Serializable
sealed interface Notification {
@Serializable(with = UUIDSerializer::class)
val id: Uuid
}
@@ -0,0 +1,11 @@
package eventDemo.shared.game.notification
import eventDemo.shared.serializers.UUIDSerializer
import kotlinx.serialization.Serializable
import kotlin.uuid.Uuid
@Serializable
data class PilesShuffledNotification(
@Serializable(with = UUIDSerializer::class)
override val id: Uuid = Uuid.random(),
) : Notification
@@ -0,0 +1,13 @@
package eventDemo.shared.game.notification
import eventDemo.shared.game.Player
import eventDemo.shared.serializers.UUIDSerializer
import kotlinx.serialization.Serializable
import kotlin.uuid.Uuid
@Serializable
data class PlayerAsJoinTheGameNotification(
@Serializable(with = UUIDSerializer::class)
override val id: Uuid = Uuid.random(),
val player: Player,
) : Notification
@@ -0,0 +1,15 @@
package eventDemo.shared.game.notification
import eventDemo.shared.game.Card
import eventDemo.shared.game.Player
import eventDemo.shared.serializers.UUIDSerializer
import kotlinx.serialization.Serializable
import kotlin.uuid.Uuid
@Serializable
data class PlayerAsPlayACardNotification(
@Serializable(with = UUIDSerializer::class)
override val id: Uuid = Uuid.random(),
val playerId: Player.PlayerId,
val card: Card,
) : Notification
@@ -0,0 +1,13 @@
package eventDemo.shared.game.notification
import eventDemo.shared.game.Player
import eventDemo.shared.serializers.UUIDSerializer
import kotlinx.serialization.Serializable
import kotlin.uuid.Uuid
@Serializable
data class PlayerHavePassNotification(
@Serializable(with = UUIDSerializer::class)
override val id: Uuid = Uuid.random(),
val playerId: Player.PlayerId,
) : Notification
@@ -0,0 +1,13 @@
package eventDemo.shared.game.notification
import eventDemo.shared.game.Player
import eventDemo.shared.serializers.UUIDSerializer
import kotlinx.serialization.Serializable
import kotlin.uuid.Uuid
@Serializable
data class PlayerWasReadyNotification(
@Serializable(with = UUIDSerializer::class)
override val id: Uuid = Uuid.random(),
val playerId: Player.PlayerId,
) : Notification
@@ -0,0 +1,13 @@
package eventDemo.shared.game.notification
import eventDemo.shared.game.Player
import eventDemo.shared.serializers.UUIDSerializer
import kotlinx.serialization.Serializable
import kotlin.uuid.Uuid
@Serializable
data class PlayerWinNotification(
@Serializable(with = UUIDSerializer::class)
override val id: Uuid = Uuid.random(),
val playerId: Player.PlayerId,
) : Notification
@@ -0,0 +1,13 @@
package eventDemo.shared.game.notification
import eventDemo.shared.game.Card
import eventDemo.shared.serializers.UUIDSerializer
import kotlinx.serialization.Serializable
import kotlin.uuid.Uuid
@Serializable
data class TheGameWasStartedNotification(
@Serializable(with = UUIDSerializer::class)
override val id: Uuid = Uuid.random(),
val hand: Set<Card>,
) : Notification
@@ -0,0 +1,13 @@
package eventDemo.shared.game.notification
import eventDemo.shared.game.Player
import eventDemo.shared.serializers.UUIDSerializer
import kotlinx.serialization.Serializable
import kotlin.uuid.Uuid
@Serializable
data class WelcomeToTheGameNotification(
@Serializable(with = UUIDSerializer::class)
override val id: Uuid = Uuid.random(),
val players: Set<Player>,
) : Notification
@@ -0,0 +1,13 @@
package eventDemo.shared.game.notification
import eventDemo.shared.game.Card
import eventDemo.shared.serializers.UUIDSerializer
import kotlinx.serialization.Serializable
import kotlin.uuid.Uuid
@Serializable
data class YourNewCardNotification(
@Serializable(with = UUIDSerializer::class)
override val id: Uuid = Uuid.random(),
val cards: Set<Card>,
) : Notification
@@ -0,0 +1,23 @@
package eventDemo.shared.game.projection
import eventDemo.shared.game.Player
import eventDemo.shared.ids.GameId
import kotlinx.serialization.Serializable
/**
* This [projection][GameProjection] is used to list all current games
*/
@Serializable
data class GameList(
val aggregateId: GameId,
val status: Status = Status.OPENING,
val players: Set<Player> = emptySet(),
val winners: Set<Player.PlayerId> = emptySet(),
) : GameProjection {
enum class Status {
OPENING,
IS_STARTED,
FINISH,
CANCELED,
}
}
@@ -0,0 +1,6 @@
package eventDemo.shared.game.projection
import kotlinx.serialization.Serializable
@Serializable
sealed interface GameProjection
@@ -0,0 +1,19 @@
package eventDemo.shared.http
import kotlinx.serialization.Serializable
@Serializable
class HttpErrorBadRequest(
val title: String = "Bad Request",
val invalidParams: List<InvalidParam> = emptyList(),
) {
// Hardcoded rather than derived from io.ktor.http.HttpStatusCode.BadRequest, since `shared`
// (multiplatform, no Ktor dependency) cannot depend on the Ktor server APIs backend uses.
val statusCode: Int = 400
@Serializable
data class InvalidParam(
val name: String,
val reason: String,
)
}
@@ -0,0 +1,11 @@
package eventDemo.shared.ids
import kotlin.uuid.Uuid
/**
* Represent an ID for one aggregate, and it used in events
* @see eventDemo.libs.eventSource.Event
*/
interface AggregateId {
val id: Uuid
}
@@ -0,0 +1,20 @@
package eventDemo.shared.ids
import eventDemo.shared.serializers.CommandIdSerializer
import kotlin.jvm.JvmInline
import kotlinx.serialization.Serializable
import kotlin.uuid.Uuid
/**
* An ID for the [eventDemo.shared.command.Command]
*/
@JvmInline
@Serializable(with = CommandIdSerializer::class)
value class CommandId(
private val id: Uuid = Uuid.random(),
) {
constructor(id: String) : this(Uuid.parse(id))
override fun toString(): String =
id.toString()
}
@@ -0,0 +1,13 @@
package eventDemo.shared.ids
import eventDemo.shared.serializers.UUIDSerializer
import kotlin.jvm.JvmInline
import kotlinx.serialization.Serializable
import kotlin.uuid.Uuid
@JvmInline
@Serializable
value class EventId(
@Serializable(with = UUIDSerializer::class)
val id: Uuid = Uuid.random(),
)
@@ -0,0 +1,19 @@
package eventDemo.shared.ids
import eventDemo.shared.serializers.UUIDSerializer
import kotlin.jvm.JvmInline
import kotlinx.serialization.Serializable
import kotlin.uuid.Uuid
/**
* An [AggregateId] for a game.
*/
@JvmInline
@Serializable
value class GameId(
@Serializable(with = UUIDSerializer::class)
override val id: Uuid = Uuid.random(),
) : AggregateId {
override fun toString(): String =
id.toString()
}
@@ -0,0 +1,13 @@
package eventDemo.shared.ids
import eventDemo.shared.serializers.UUIDSerializer
import kotlin.jvm.JvmInline
import kotlinx.serialization.Serializable
import kotlin.uuid.Uuid
@Serializable
@JvmInline
value class UserId(
@Serializable(with = UUIDSerializer::class)
override val id: Uuid = Uuid.random(),
) : AggregateId
@@ -0,0 +1,23 @@
package eventDemo.shared.serializers
import eventDemo.shared.ids.CommandId
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
object CommandIdSerializer : KSerializer<CommandId> {
override fun deserialize(decoder: Decoder): CommandId =
CommandId(decoder.decodeString())
override fun serialize(
encoder: Encoder,
value: CommandId,
) {
encoder.encodeString(value.toString())
}
override val descriptor: SerialDescriptor = PrimitiveSerialDescriptor("CommandId", PrimitiveKind.STRING)
}
@@ -0,0 +1,24 @@
package eventDemo.shared.serializers
import eventDemo.shared.ids.EventId
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
import kotlin.uuid.Uuid
object EventIdSerializer : KSerializer<EventId> {
override fun deserialize(decoder: Decoder): EventId =
EventId(Uuid.parse(decoder.decodeString()))
override fun serialize(
encoder: Encoder,
value: EventId,
) {
encoder.encodeString(value.id.toString())
}
override val descriptor: SerialDescriptor = PrimitiveSerialDescriptor("EventId", PrimitiveKind.STRING)
}
@@ -0,0 +1,24 @@
package eventDemo.shared.serializers
import eventDemo.shared.ids.GameId
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
import kotlin.uuid.Uuid
object GameIdSerializer : KSerializer<GameId> {
override fun deserialize(decoder: Decoder): GameId =
GameId(Uuid.parse(decoder.decodeString()))
override fun serialize(
encoder: Encoder,
value: GameId,
) {
encoder.encodeString(value.id.toString())
}
override val descriptor: SerialDescriptor = PrimitiveSerialDescriptor("GameId", PrimitiveKind.STRING)
}
@@ -0,0 +1,24 @@
package eventDemo.shared.serializers
import eventDemo.shared.game.Player
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
import kotlin.uuid.Uuid
object PlayerIdSerializer : KSerializer<Player.PlayerId> {
override fun deserialize(decoder: Decoder): Player.PlayerId =
Player.PlayerId(Uuid.parse(decoder.decodeString()))
override fun serialize(
encoder: Encoder,
value: Player.PlayerId,
) {
encoder.encodeString(value.id.toString())
}
override val descriptor: SerialDescriptor = PrimitiveSerialDescriptor("PlayerId", PrimitiveKind.STRING)
}
@@ -0,0 +1,23 @@
package eventDemo.shared.serializers
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
import kotlin.uuid.Uuid
object UUIDSerializer : KSerializer<Uuid> {
override fun deserialize(decoder: Decoder): Uuid =
Uuid.parse(decoder.decodeString())
override fun serialize(
encoder: Encoder,
value: Uuid,
) {
encoder.encodeString(value.toString())
}
override val descriptor: SerialDescriptor = PrimitiveSerialDescriptor("UUID", PrimitiveKind.STRING)
}