fix(game): #59 — per-command rejection on PUT /api/v1/order
Validation of a player's order now applies every command against a transient game-state snapshot and records the per-command outcome (cmdApplied, cmdErrorCode) in each command's meta. The order is persisted even when some commands are rejected, and the response is 202 + UserGamesOrder so clients can surface the partial failure without the chain collapsing into "downstream service is unavailable". Pkg/error consts are reshelved onto three explicit ranges with a package doc and helpers (IsInternalCode/IsInputCode/IsGameStateCode): 1xxx internal/server (500/501), 2xxx structural input (400), 3xxx game-state per-command rejection (400 when escaping HTTP, otherwise recorded as cmdErrorCode). Two pre-existing typos fixed mechanically (ErrBeakGroupNumberNotEnough -> ErrBreakGroupNumberNotEnough, ErrRaceExinct -> ErrRaceExtinct) along with all callsites. Engine errorResponse maps *GenericError by shelf rather than mapping everything to 500. The Quit-not-last structural check in Controller.ValidateOrder is preserved and its type assertion fixed (was a value assertion against a pointer-typed command, so the check silently never fired). Backend, gateway and UI are unchanged — they were already correct on the 202 path; only the engine collapsing per-command rejection into 500 was needed. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -142,6 +142,18 @@ func stateResponse(s game.State) rest.StateResponse {
|
||||
return *result
|
||||
}
|
||||
|
||||
// errorResponse renders err onto c and reports whether the caller
|
||||
// should stop further processing. The HTTP status is selected by the
|
||||
// GenericError shelf (see pkg/error for the taxonomy):
|
||||
//
|
||||
// - validator.ValidationErrors (request struct binding) → 400 with
|
||||
// {"error": ...}.
|
||||
// - GenericError, ErrGameNotInitialized → 501 with no body.
|
||||
// - GenericError on the internal shelf (1xxx) → 500 with
|
||||
// {"generic_error", "code"}.
|
||||
// - GenericError on the input-validation shelf (2xxx) or the
|
||||
// game-state shelf (3xxx) → 400 with {"generic_error", "code"}.
|
||||
// - everything else (non-GenericError) → 500 with {"error": ...}.
|
||||
func errorResponse(c *gin.Context, err error) bool {
|
||||
if err == nil {
|
||||
return false
|
||||
@@ -153,9 +165,11 @@ func errorResponse(c *gin.Context, err error) bool {
|
||||
}
|
||||
|
||||
if ge, ok := errors.AsType[*e.GenericError](err); ok {
|
||||
switch ge.Code {
|
||||
case e.ErrGameNotInitialized:
|
||||
switch {
|
||||
case ge.Code == e.ErrGameNotInitialized:
|
||||
c.Status(http.StatusNotImplemented)
|
||||
case e.IsInputCode(ge.Code), e.IsGameStateCode(ge.Code):
|
||||
c.JSON(http.StatusBadRequest, gin.H{"generic_error": ge.Error(), "code": ge.Code})
|
||||
default:
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"generic_error": ge.Error(), "code": ge.Code})
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user