docs: reorder & testing
This commit is contained in:
@@ -0,0 +1,247 @@
|
||||
package notification
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"galaxy/backend/push"
|
||||
"galaxy/transcoder"
|
||||
|
||||
"github.com/google/uuid"
|
||||
)
|
||||
|
||||
// preMarshaledEvent adapts a pre-encoded FlatBuffers payload to the
|
||||
// push.Event interface. The factory below pre-encodes the payload at
|
||||
// construction time so the kind-specific build error surfaces inside
|
||||
// the dispatcher (where it can drive retry / dead-letter logic) rather
|
||||
// than inside push.Service.PublishClientEvent.
|
||||
type preMarshaledEvent struct {
|
||||
kind string
|
||||
payload []byte
|
||||
}
|
||||
|
||||
func (e preMarshaledEvent) Kind() string { return e.kind }
|
||||
func (e preMarshaledEvent) Marshal() ([]byte, error) { return e.payload, nil }
|
||||
|
||||
// buildClientPushEvent maps a catalog kind together with the producer
|
||||
// payload map onto a typed push.Event. Every catalog kind has a
|
||||
// FlatBuffers schema in `pkg/schema/fbs/notification.fbs`; an unknown
|
||||
// kind falls back to push.JSONEvent so a misconfigured producer keeps
|
||||
// the pipeline flowing while the catalog catches up.
|
||||
func buildClientPushEvent(kind string, payload map[string]any) (push.Event, error) {
|
||||
switch kind {
|
||||
case KindLobbyInviteReceived:
|
||||
gameID, err := mapUUID(payload, "game_id")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
inviter, err := mapUUID(payload, "inviter_user_id")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
bytes, err := transcoder.LobbyInviteReceivedEventToPayload(&transcoder.LobbyInviteReceivedEvent{
|
||||
GameID: gameID,
|
||||
InviterUserID: inviter,
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return preMarshaledEvent{kind: kind, payload: bytes}, nil
|
||||
|
||||
case KindLobbyInviteRevoked:
|
||||
gameID, err := mapUUID(payload, "game_id")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
bytes, err := transcoder.LobbyInviteRevokedEventToPayload(&transcoder.LobbyInviteRevokedEvent{GameID: gameID})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return preMarshaledEvent{kind: kind, payload: bytes}, nil
|
||||
|
||||
case KindLobbyApplicationSubmitted:
|
||||
gameID, err := mapUUID(payload, "game_id")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
appID, err := mapUUID(payload, "application_id")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
bytes, err := transcoder.LobbyApplicationSubmittedEventToPayload(&transcoder.LobbyApplicationSubmittedEvent{
|
||||
GameID: gameID,
|
||||
ApplicationID: appID,
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return preMarshaledEvent{kind: kind, payload: bytes}, nil
|
||||
|
||||
case KindLobbyApplicationApproved:
|
||||
gameID, err := mapUUID(payload, "game_id")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
bytes, err := transcoder.LobbyApplicationApprovedEventToPayload(&transcoder.LobbyApplicationApprovedEvent{GameID: gameID})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return preMarshaledEvent{kind: kind, payload: bytes}, nil
|
||||
|
||||
case KindLobbyApplicationRejected:
|
||||
gameID, err := mapUUID(payload, "game_id")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
bytes, err := transcoder.LobbyApplicationRejectedEventToPayload(&transcoder.LobbyApplicationRejectedEvent{GameID: gameID})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return preMarshaledEvent{kind: kind, payload: bytes}, nil
|
||||
|
||||
case KindLobbyMembershipRemoved:
|
||||
bytes, err := transcoder.LobbyMembershipRemovedEventToPayload(&transcoder.LobbyMembershipRemovedEvent{
|
||||
Reason: mapStringOpt(payload, "reason"),
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return preMarshaledEvent{kind: kind, payload: bytes}, nil
|
||||
|
||||
case KindLobbyMembershipBlocked:
|
||||
gameID, err := mapUUID(payload, "game_id")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
bytes, err := transcoder.LobbyMembershipBlockedEventToPayload(&transcoder.LobbyMembershipBlockedEvent{
|
||||
GameID: gameID,
|
||||
Reason: mapStringOpt(payload, "reason"),
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return preMarshaledEvent{kind: kind, payload: bytes}, nil
|
||||
|
||||
case KindLobbyRaceNameRegistered:
|
||||
raceName, err := mapString(payload, "race_name")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
bytes, err := transcoder.LobbyRaceNameRegisteredEventToPayload(&transcoder.LobbyRaceNameRegisteredEvent{RaceName: raceName})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return preMarshaledEvent{kind: kind, payload: bytes}, nil
|
||||
|
||||
case KindLobbyRaceNamePending:
|
||||
raceName, err := mapString(payload, "race_name")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
bytes, err := transcoder.LobbyRaceNamePendingEventToPayload(&transcoder.LobbyRaceNamePendingEvent{
|
||||
RaceName: raceName,
|
||||
ExpiresAt: mapStringOpt(payload, "expires_at"),
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return preMarshaledEvent{kind: kind, payload: bytes}, nil
|
||||
|
||||
case KindLobbyRaceNameExpired:
|
||||
raceName, err := mapString(payload, "race_name")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
bytes, err := transcoder.LobbyRaceNameExpiredEventToPayload(&transcoder.LobbyRaceNameExpiredEvent{RaceName: raceName})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return preMarshaledEvent{kind: kind, payload: bytes}, nil
|
||||
|
||||
case KindRuntimeImagePullFailed:
|
||||
gameID, err := mapUUID(payload, "game_id")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
bytes, err := transcoder.RuntimeImagePullFailedEventToPayload(&transcoder.RuntimeImagePullFailedEvent{
|
||||
GameID: gameID,
|
||||
ImageRef: mapStringOpt(payload, "image_ref"),
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return preMarshaledEvent{kind: kind, payload: bytes}, nil
|
||||
|
||||
case KindRuntimeContainerStartFailed:
|
||||
gameID, err := mapUUID(payload, "game_id")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
bytes, err := transcoder.RuntimeContainerStartFailedEventToPayload(&transcoder.RuntimeContainerStartFailedEvent{GameID: gameID})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return preMarshaledEvent{kind: kind, payload: bytes}, nil
|
||||
|
||||
case KindRuntimeStartConfigInvalid:
|
||||
gameID, err := mapUUID(payload, "game_id")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
bytes, err := transcoder.RuntimeStartConfigInvalidEventToPayload(&transcoder.RuntimeStartConfigInvalidEvent{
|
||||
GameID: gameID,
|
||||
Reason: mapStringOpt(payload, "reason"),
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return preMarshaledEvent{kind: kind, payload: bytes}, nil
|
||||
}
|
||||
|
||||
return push.JSONEvent{EventKind: kind, Payload: payload}, nil
|
||||
}
|
||||
|
||||
// mapUUID extracts a required UUID-shaped field from the producer
|
||||
// payload. Producers stringify uuid values before assembling Intent
|
||||
// payloads, so the JSON-roundtripped form is `string`.
|
||||
func mapUUID(payload map[string]any, key string) (uuid.UUID, error) {
|
||||
raw, ok := payload[key]
|
||||
if !ok {
|
||||
return uuid.Nil, fmt.Errorf("notification payload: %s is missing", key)
|
||||
}
|
||||
str, ok := raw.(string)
|
||||
if !ok {
|
||||
return uuid.Nil, fmt.Errorf("notification payload: %s must be a string, got %T", key, raw)
|
||||
}
|
||||
parsed, err := uuid.Parse(str)
|
||||
if err != nil {
|
||||
return uuid.Nil, fmt.Errorf("notification payload: %s is not a uuid: %w", key, err)
|
||||
}
|
||||
return parsed, nil
|
||||
}
|
||||
|
||||
// mapString extracts a required string field from the producer payload.
|
||||
func mapString(payload map[string]any, key string) (string, error) {
|
||||
raw, ok := payload[key]
|
||||
if !ok {
|
||||
return "", fmt.Errorf("notification payload: %s is missing", key)
|
||||
}
|
||||
str, ok := raw.(string)
|
||||
if !ok {
|
||||
return "", fmt.Errorf("notification payload: %s must be a string, got %T", key, raw)
|
||||
}
|
||||
if str == "" {
|
||||
return "", fmt.Errorf("notification payload: %s is empty", key)
|
||||
}
|
||||
return str, nil
|
||||
}
|
||||
|
||||
// mapStringOpt returns the string value for key, or "" when the key is
|
||||
// missing or carries a non-string value.
|
||||
func mapStringOpt(payload map[string]any, key string) string {
|
||||
raw, ok := payload[key]
|
||||
if !ok {
|
||||
return ""
|
||||
}
|
||||
str, _ := raw.(string)
|
||||
return str
|
||||
}
|
||||
Reference in New Issue
Block a user