feat(api): define shared EventEnvelope and EventType for Redis EventBus #61
@@ -45,6 +45,7 @@ dependencies {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
implementation(project(":modules:api"))
|
||||||
implementation(project(":modules:security"))
|
implementation(project(":modules:security"))
|
||||||
|
|
||||||
implementation(platform("${quarkusPlatformGroupId}:${quarkusPlatformArtifactId}:${quarkusPlatformVersion}"))
|
implementation(platform("${quarkusPlatformGroupId}:${quarkusPlatformArtifactId}:${quarkusPlatformVersion}"))
|
||||||
|
|||||||
@@ -12,10 +12,13 @@ import de.nowchess.account.domain.{
|
|||||||
UserAccount,
|
UserAccount,
|
||||||
}
|
}
|
||||||
import de.nowchess.account.dto.*
|
import de.nowchess.account.dto.*
|
||||||
|
import de.nowchess.api.event.{EventEnvelope, EventType}
|
||||||
import io.quarkus.runtime.annotations.RegisterForReflection
|
import io.quarkus.runtime.annotations.RegisterForReflection
|
||||||
|
|
||||||
@RegisterForReflection(
|
@RegisterForReflection(
|
||||||
targets = Array(
|
targets = Array(
|
||||||
|
classOf[EventEnvelope],
|
||||||
|
classOf[EventType],
|
||||||
classOf[UserAccount],
|
classOf[UserAccount],
|
||||||
classOf[BotAccount],
|
classOf[BotAccount],
|
||||||
classOf[OfficialBotAccount],
|
classOf[OfficialBotAccount],
|
||||||
|
|||||||
@@ -1,6 +1,8 @@
|
|||||||
package de.nowchess.account.service
|
package de.nowchess.account.service
|
||||||
|
|
||||||
|
import com.fasterxml.jackson.databind.ObjectMapper
|
||||||
import de.nowchess.account.config.RedisConfig
|
import de.nowchess.account.config.RedisConfig
|
||||||
|
import de.nowchess.api.event.{EventEnvelope, EventType}
|
||||||
import io.quarkus.redis.datasource.RedisDataSource
|
import io.quarkus.redis.datasource.RedisDataSource
|
||||||
import jakarta.enterprise.context.ApplicationScoped
|
import jakarta.enterprise.context.ApplicationScoped
|
||||||
import jakarta.inject.Inject
|
import jakarta.inject.Inject
|
||||||
@@ -10,22 +12,36 @@ import scala.compiletime.uninitialized
|
|||||||
class EventPublisher:
|
class EventPublisher:
|
||||||
|
|
||||||
// scalafix:off DisableSyntax.var
|
// scalafix:off DisableSyntax.var
|
||||||
@Inject var redis: RedisDataSource = uninitialized
|
@Inject var redis: RedisDataSource = uninitialized
|
||||||
@Inject var redisConfig: RedisConfig = uninitialized
|
@Inject var redisConfig: RedisConfig = uninitialized
|
||||||
|
@Inject var objectMapper: ObjectMapper = uninitialized
|
||||||
// scalafix:on DisableSyntax.var
|
// scalafix:on DisableSyntax.var
|
||||||
|
|
||||||
def publishGameStart(botId: String, gameId: String, playingAs: String, difficulty: Int, botAccountId: String): Unit =
|
def publishGameStart(botId: String, gameId: String, playingAs: String, difficulty: Int, botAccountId: String): Unit =
|
||||||
val event =
|
val payload = objectMapper.createObjectNode()
|
||||||
s"""{"type":"gameStart","gameId":"$gameId","playingAs":"$playingAs","difficulty":$difficulty,"botAccountId":"$botAccountId"}"""
|
payload.put("gameId", gameId)
|
||||||
redis.pubsub(classOf[String]).publish(s"${redisConfig.prefix}:bot:$botId:events", event)
|
payload.put("playingAs", playingAs)
|
||||||
()
|
payload.put("difficulty", difficulty)
|
||||||
|
payload.put("botAccountId", botAccountId)
|
||||||
|
publish(s"${redisConfig.prefix}:bot:$botId:events", EventType.GameStart, payload)
|
||||||
|
|
||||||
def publishChallengeCreated(destUserId: String, challengeId: String, challengerName: String): Unit =
|
def publishChallengeCreated(destUserId: String, challengeId: String, challengerName: String): Unit =
|
||||||
val event = s"""{"type":"challengeCreated","challengeId":"$challengeId","challengerName":"$challengerName"}"""
|
val payload = objectMapper.createObjectNode()
|
||||||
redis.pubsub(classOf[String]).publish(s"${redisConfig.prefix}:user:$destUserId:events", event)
|
payload.put("challengeId", challengeId)
|
||||||
()
|
payload.put("challengerName", challengerName)
|
||||||
|
publish(s"${redisConfig.prefix}:user:$destUserId:events", EventType.ChallengeCreated, payload)
|
||||||
|
|
||||||
def publishChallengeAccepted(challengerId: String, challengeId: String, gameId: String): Unit =
|
def publishChallengeAccepted(challengerId: String, challengeId: String, gameId: String): Unit =
|
||||||
val event = s"""{"type":"challengeAccepted","challengeId":"$challengeId","gameId":"$gameId"}"""
|
val payload = objectMapper.createObjectNode()
|
||||||
redis.pubsub(classOf[String]).publish(s"${redisConfig.prefix}:user:$challengerId:events", event)
|
payload.put("challengeId", challengeId)
|
||||||
|
payload.put("gameId", gameId)
|
||||||
|
publish(s"${redisConfig.prefix}:user:$challengerId:events", EventType.ChallengeAccepted, payload)
|
||||||
|
|
||||||
|
private def publish(
|
||||||
|
channel: String,
|
||||||
|
eventType: EventType,
|
||||||
|
payload: com.fasterxml.jackson.databind.node.ObjectNode,
|
||||||
|
): Unit =
|
||||||
|
val envelope = EventEnvelope.of(eventType, payload)
|
||||||
|
redis.pubsub(classOf[String]).publish(channel, objectMapper.writeValueAsString(envelope))
|
||||||
()
|
()
|
||||||
|
|||||||
@@ -50,6 +50,8 @@ dependencies {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
implementation("com.fasterxml.jackson.core:jackson-databind:${versions["JACKSON"]!!}")
|
implementation("com.fasterxml.jackson.core:jackson-databind:${versions["JACKSON"]!!}")
|
||||||
|
implementation("com.fasterxml.jackson.module:jackson-module-scala_3:${versions["JACKSON_SCALA"]!!}")
|
||||||
|
implementation("com.fasterxml.jackson.datatype:jackson-datatype-jsr310:${versions["JACKSON"]!!}")
|
||||||
|
|
||||||
testImplementation(platform("org.junit:junit-bom:5.13.4"))
|
testImplementation(platform("org.junit:junit-bom:5.13.4"))
|
||||||
testImplementation("org.junit.jupiter:junit-jupiter")
|
testImplementation("org.junit.jupiter:junit-jupiter")
|
||||||
|
|||||||
@@ -0,0 +1,27 @@
|
|||||||
|
package de.nowchess.api.event
|
||||||
|
|
||||||
|
import com.fasterxml.jackson.databind.JsonNode
|
||||||
|
import java.time.Instant
|
||||||
|
import java.util.UUID
|
||||||
|
|
||||||
|
final case class EventEnvelope(
|
||||||
|
eventId: UUID,
|
||||||
|
`type`: EventType,
|
||||||
|
payload: JsonNode,
|
||||||
|
timestamp: Instant,
|
||||||
|
correlationId: Option[String],
|
||||||
|
)
|
||||||
|
|
||||||
|
object EventEnvelope:
|
||||||
|
def of(
|
||||||
|
`type`: EventType,
|
||||||
|
payload: JsonNode,
|
||||||
|
correlationId: Option[String] = None,
|
||||||
|
): EventEnvelope =
|
||||||
|
EventEnvelope(
|
||||||
|
eventId = UUID.randomUUID(),
|
||||||
|
`type` = `type`,
|
||||||
|
payload = payload,
|
||||||
|
timestamp = Instant.now(),
|
||||||
|
correlationId = correlationId,
|
||||||
|
)
|
||||||
@@ -0,0 +1,4 @@
|
|||||||
|
package de.nowchess.api.event
|
||||||
|
|
||||||
|
enum EventType:
|
||||||
|
case GameStart, GameCreationRequest, GameCreationResponse, BotGameStart, ChallengeCreated, ChallengeAccepted
|
||||||
@@ -0,0 +1,50 @@
|
|||||||
|
package de.nowchess.api.event
|
||||||
|
|
||||||
|
import com.fasterxml.jackson.databind.ObjectMapper
|
||||||
|
import com.fasterxml.jackson.module.scala.DefaultScalaModule
|
||||||
|
import org.scalatest.funsuite.AnyFunSuite
|
||||||
|
import org.scalatest.matchers.should.Matchers
|
||||||
|
|
||||||
|
class EventEnvelopeTest extends AnyFunSuite with Matchers:
|
||||||
|
|
||||||
|
private val mapper =
|
||||||
|
val m = new ObjectMapper()
|
||||||
|
m.registerModule(DefaultScalaModule)
|
||||||
|
m.findAndRegisterModules()
|
||||||
|
m
|
||||||
|
|
||||||
|
test("EventEnvelope round-trips through JSON") {
|
||||||
|
val payload = mapper.createObjectNode()
|
||||||
|
payload.put("gameId", "game-123")
|
||||||
|
payload.put("difficulty", 3)
|
||||||
|
|
||||||
|
val original = EventEnvelope.of(EventType.GameStart, payload, Some("corr-abc"))
|
||||||
|
|
||||||
|
val json = mapper.writeValueAsString(original)
|
||||||
|
val decoded = mapper.readValue(json, classOf[EventEnvelope])
|
||||||
|
|
||||||
|
decoded.eventId shouldBe original.eventId
|
||||||
|
decoded.`type` shouldBe original.`type`
|
||||||
|
decoded.payload shouldBe original.payload
|
||||||
|
decoded.timestamp shouldBe original.timestamp
|
||||||
|
decoded.correlationId shouldBe Some("corr-abc")
|
||||||
|
}
|
||||||
|
|
||||||
|
test("EventEnvelope serializes without correlationId") {
|
||||||
|
val payload = mapper.createObjectNode()
|
||||||
|
payload.put("challengeId", "ch-1")
|
||||||
|
|
||||||
|
val envelope = EventEnvelope.of(EventType.ChallengeCreated, payload)
|
||||||
|
val json = mapper.writeValueAsString(envelope)
|
||||||
|
val decoded = mapper.readValue(json, classOf[EventEnvelope])
|
||||||
|
|
||||||
|
decoded.`type` shouldBe EventType.ChallengeCreated
|
||||||
|
decoded.correlationId shouldBe None
|
||||||
|
}
|
||||||
|
|
||||||
|
test("EventEnvelope.of generates unique eventIds") {
|
||||||
|
val payload = mapper.createObjectNode()
|
||||||
|
val e1 = EventEnvelope.of(EventType.BotGameStart, payload)
|
||||||
|
val e2 = EventEnvelope.of(EventType.BotGameStart, payload)
|
||||||
|
e1.eventId should not equal e2.eventId
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user