Skip to content
Open
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 libs/common/docs/statik/statik.go

Large diffs are not rendered by default.

66 changes: 65 additions & 1 deletion libs/common/docs/swagger/gameplay/gameplay.swagger.json
Original file line number Diff line number Diff line change
Expand Up @@ -1419,7 +1419,7 @@
},
"numRounds": {
"type": "integer",
"format": "int32"
"format": "int64"
},
"currentArtist": {
"type": "string"
Expand Down Expand Up @@ -1537,6 +1537,9 @@
"properties": {
"game": {
"$ref": "#/definitions/v1CurrentGameInfo"
},
"lastCompletedRound": {
"$ref": "#/definitions/v1RoundSummary"
}
}
},
Expand Down Expand Up @@ -1990,6 +1993,67 @@
}
}
},
"v1RoundStanding": {
"type": "object",
"properties": {
"playerId": {
"type": "string"
},
"guessTimeMs": {
"type": "string",
"format": "int64"
},
"tier": {
"type": "string"
},
"roundPosition": {
"type": "string",
"format": "int64"
},
"roundPoints": {
"type": "string",
"format": "int64"
},
"roundStakeLost": {
"type": "string"
},
"totalPoints": {
"type": "string",
"format": "int64"
},
"totalStakeLost": {
"type": "string"
},
"provisionalPayout": {
"type": "string"
}
}
},
"v1RoundSummary": {
"type": "object",
"properties": {
"round": {
"type": "integer",
"format": "int64"
},
"artistId": {
"type": "string"
},
"word": {
"type": "string"
},
"totalPot": {
"type": "string"
},
"standings": {
"type": "array",
"items": {
"type": "object",
"$ref": "#/definitions/v1RoundStanding"
}
}
}
},
"v1Server": {
"type": "object",
"properties": {
Expand Down
50 changes: 36 additions & 14 deletions libs/common/server/server.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ package server
import (
"context"
"errors"
"fmt"
"net"
"net/http"
"time"
Expand Down Expand Up @@ -102,11 +103,13 @@ RunGRPCServer starts a gRPC server on the given address. It accepts a slice of
GrpcMiddlewareProvider to chain unary interceptors, and a slice of GrpcServiceRegistrar
to register service implementations.

Panics on failure to listen or serve.
Returns an error on failure to listen or serve.

Example usage:

server.RunGRPCServer(
err := server.RunGRPCServer(
ctx,
waitGroup,
":9090",
[]GrpcMiddlewareProvider{
func() grpc.UnaryServerInterceptor { return middleware.GrpcExtractMetadata },
Expand All @@ -125,7 +128,7 @@ func RunGRPCServer(
address string,
middlewareProviders []GrpcMiddlewareProvider,
serviceRegistrars []GrpcServiceRegistrar,
) {
) error {
interceptors := []grpc.UnaryServerInterceptor{middleware.GrpcExtractMetadata}

for _, provider := range middlewareProviders {
Expand All @@ -144,7 +147,7 @@ func RunGRPCServer(

listener, err := net.Listen("tcp", address)
if err != nil {
log.Fatal().Err(err).Msg("failed to create gRPC listener")
return fmt.Errorf("failed to listen on %s: %w", address, err)
}

waitGroup.Go(func() error {
Expand All @@ -170,13 +173,15 @@ func RunGRPCServer(

return nil
})

return nil
}

/*
RunGatewayServer starts an HTTP server with gRPC-Gateway routing, optional HTTP routes,
and a custom HTTP middleware stack. It also mounts Swagger docs in development mode.

Panics on failure to listen or serve.
Returns an error on failure to listen or serve.

Parameters:
- address: HTTP server listen address
Expand All @@ -187,9 +192,12 @@ Parameters:

Example usage:

server.RunGatewayServer(
err := server.RunGatewayServer(
ctx,
waitGroup,
":8080",
true,
[]string{"http://*.spazzle.io"},
[]server.GatewayRouteRegistrar{
registerAuthGatewayHandler(authServer),
},
Expand All @@ -208,7 +216,7 @@ func RunGatewayServer(
routeRegistrars []GatewayRouteRegistrar,
httpRouteRegistrars []HttpRouteRegistrar,
middlewareBuilder HttpMiddlewareBuilder,
) {
) error {
opt := runtime.WithMarshalerOption(runtime.MIMEWildcard, &runtime.JSONPb{
MarshalOptions: protojson.MarshalOptions{
EmitDefaultValues: true,
Expand All @@ -223,7 +231,7 @@ func RunGatewayServer(

for _, registrar := range routeRegistrars {
if err := registrar(ctx, grpcMux); err != nil {
log.Fatal().Err(err).Msg("failed to register gateway route")
return fmt.Errorf("failed to register gateway route: %w", err)
}
}

Expand All @@ -235,7 +243,11 @@ func RunGatewayServer(
}

if isDevelopmentEnvironment {
mux = serveSwagger(mux)
var err error
mux, err = serveSwagger(mux)
if err != nil {
return err
}
}

handler := middleware.HTTPExtractMetadata(
Expand Down Expand Up @@ -268,9 +280,14 @@ func RunGatewayServer(
IdleTimeout: 120 * time.Second,
}

listener, err := net.Listen("tcp", address)
if err != nil {
return fmt.Errorf("failed to listen on HTTP address %s: %w", address, err)
}

waitGroup.Go(func() error {
log.Info().Msgf("started HTTP gateway server at %s", srv.Addr)
if err := srv.ListenAndServe(); err != nil {
if err := srv.Serve(listener); err != nil {
if errors.Is(err, http.ErrServerClosed) {
return nil
}
Expand All @@ -285,23 +302,28 @@ func RunGatewayServer(
<-ctx.Done()
log.Info().Msg("shutting down HTTP server")

if err := srv.Shutdown(context.Background()); err != nil {
shutdownCtx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()

if err := srv.Shutdown(shutdownCtx); err != nil {
log.Error().Err(err).Msg("failed to shutdown HTTP server")
}

log.Info().Msg("HTTP server stopped")
return nil
})

return nil
}

func serveSwagger(mux *http.ServeMux) *http.ServeMux {
func serveSwagger(mux *http.ServeMux) (*http.ServeMux, error) {
statikFS, err := fs.New()
if err != nil {
log.Fatal().Err(err).Msg("cannot create statik fs")
return nil, fmt.Errorf("failed to create statik fs: %v", err)
}

swaggerHandler := http.StripPrefix("/swagger/", http.FileServer(statikFS))
mux.Handle("/swagger/", swaggerHandler)

return mux
return mux, nil
}
18 changes: 18 additions & 0 deletions libs/common/util/util.go
Original file line number Diff line number Diff line change
Expand Up @@ -85,6 +85,24 @@ func Uint32ToUint8(n uint32) (uint8, error) {
return uint8(n), nil
}

// Int32ToUint8 safely converts int32 to uint8 with bounds checking.
// Returns an error if the value is negative or exceeds math.MaxUint8.
func Int32ToUint8(n int32) (uint8, error) {
if n < 0 || n > math.MaxUint8 {
return 0, fmt.Errorf("int32 value %d out of uint8 range", n)
}
return uint8(n), nil
}

// IntToUint32 safely converts int to uint32 with bounds checking.
// Returns an error if the value is negative or exceeds math.MaxUint32.
func IntToUint32(n int) (uint32, error) {
if n < 0 || uint64(n) > math.MaxUint32 {
return 0, fmt.Errorf("int value %d out of uint32 range", n)
}
return uint32(n), nil
}

// IntToInt32 safely converts int to int32 with bounds checking.
// Returns an error if the value cannot fit in int32.
func IntToInt32(n int) (int32, error) {
Expand Down
88 changes: 88 additions & 0 deletions libs/common/util/util_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -239,6 +239,94 @@ func TestUInt32ToUInt8(t *testing.T) {
}
}

func TestInt32ToUInt8(t *testing.T) {
testCases := []struct {
name string
input int32
expected uint8
success bool
}{
{
name: "success",
input: 12,
expected: 12,
success: true,
},
{
name: "zero",
input: int32(0),
success: true,
},
{
name: "input too large",
input: math.MaxInt32,
success: false,
},
{
name: "negative",
input: int32(-4),
success: false,
},
}

for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
result, err := Int32ToUint8(tc.input)
if tc.success {
require.NoError(t, err)
require.Equal(t, tc.expected, result)
return
}

require.Error(t, err)
})
}
}

func TestIntToUint32(t *testing.T) {
testCases := []struct {
name string
input int
expected uint32
success bool
}{
{
name: "success",
input: 12,
expected: 12,
success: true,
},
{
name: "zero",
input: 0,
success: true,
},
{
name: "input too large",
input: math.MaxInt64,
success: false,
},
{
name: "negative",
input: -4,
success: false,
},
}

for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
result, err := IntToUint32(tc.input)
if tc.success {
require.NoError(t, err)
require.Equal(t, tc.expected, result)
return
}

require.Error(t, err)
})
}
}

func TestIntToInt32(t *testing.T) {
testCases := []struct {
name string
Expand Down
Loading
Loading