From a477f5ce0b10e6a1df7f803662f3828ab10285db Mon Sep 17 00:00:00 2001 From: Ilia Denisov Date: Wed, 11 Feb 2026 00:30:37 +0200 Subject: [PATCH] feat: more validators --- internal/controller/command.go | 14 +- internal/controller/controller_export_test.go | 4 + internal/controller/ship_group.go | 2 +- internal/controller/ship_group_test.go | 17 +++ internal/model/game/group.go | 7 +- internal/model/game/route.go | 10 +- internal/model/rest/command.go | 137 +++++++++++++++++- internal/router/command_test.go | 2 +- internal/router/handler/command.go | 41 +++--- internal/router/router.go | 6 + internal/router/validator.go | 22 +++ internal/util/string.go | 1 + 12 files changed, 224 insertions(+), 39 deletions(-) diff --git a/internal/controller/command.go b/internal/controller/command.go index 53468b0..7003ef5 100644 --- a/internal/controller/command.go +++ b/internal/controller/command.go @@ -83,7 +83,7 @@ func (c *Controller) ShipGroupLoad(actor string, groupID uuid.UUID, cargoType st if err != nil { return err } - ct, ok := game.CargoTypeSet[cargoType] + ct, ok := game.CargoTypeSet[strings.ToLower(cargoType)] if !ok { return e.NewCargoTypeInvalidError(cargoType) } @@ -151,12 +151,12 @@ func (c *Controller) ShipGroupTransfer(actor, acceptor string, groupID uuid.UUID return c.Cache.shipGroupTransfer(ri, riAccept, groupID, quantity) } -func (c *Controller) ShipGroupJoinFleet(actor, fleetName string, groupID uuid.UUID, count uint) error { +func (c *Controller) ShipGroupJoinFleet(actor, fleetName string, groupID uuid.UUID, quantity uint) error { ri, err := c.Cache.validActor(actor) if err != nil { return err } - return c.Cache.ShipGroupJoinFleet(ri, fleetName, groupID, count) + return c.Cache.ShipGroupJoinFleet(ri, fleetName, groupID, quantity) } func (c *Controller) FleetMerge(actor, fleetSourceName, fleetTargetName string) error { @@ -195,12 +195,12 @@ func (c *Controller) ScienceRemove(actor, typeName string) error { return c.Cache.ScienceRemove(ri, typeName) } -func (c *Controller) PlanetRename(actor string, planetNumber int, typeName string) error { +func (c *Controller) PlanetRename(actor string, planetNumber int, name string) error { ri, err := c.Cache.validActor(actor) if err != nil { return err } - return c.Cache.PlanetRename(ri, planetNumber, typeName) + return c.Cache.PlanetRename(ri, planetNumber, name) } func (c *Controller) PlanetProduce(actor string, planetNumber int, prodType, subject string) error { @@ -237,7 +237,7 @@ func (c *Controller) PlanetRouteSet(actor, loadType string, origin, destination if err != nil { return err } - rt, ok := game.RouteTypeSet[loadType] + rt, ok := game.RouteTypeSet[strings.ToLower(loadType)] if !ok { return e.NewCargoTypeInvalidError(loadType) } @@ -249,7 +249,7 @@ func (c *Controller) PlanetRouteRemove(actor, loadType string, origin uint) erro if err != nil { return err } - rt, ok := game.RouteTypeSet[loadType] + rt, ok := game.RouteTypeSet[strings.ToLower(loadType)] if !ok { return e.NewCargoTypeInvalidError(loadType) } diff --git a/internal/controller/controller_export_test.go b/internal/controller/controller_export_test.go index eeb8c0c..deb3c6e 100644 --- a/internal/controller/controller_export_test.go +++ b/internal/controller/controller_export_test.go @@ -143,3 +143,7 @@ func (c *Cache) CreateShipsUnsafe_T(ri int, classID uuid.UUID, planet uint, quan func (c *Cache) WipeRace(ri int) { c.wipeRace(ri) } + +func (c *Cache) UnsafeDeleteShipGroup(sgi int) { + c.unsafeDeleteShipGroup(sgi) +} diff --git a/internal/controller/ship_group.go b/internal/controller/ship_group.go index 5d8ab10..1424da5 100644 --- a/internal/controller/ship_group.go +++ b/internal/controller/ship_group.go @@ -407,7 +407,6 @@ func (c *Cache) shipGroupTransfer(ri, riAccept int, groupID uuid.UUID, quantity } if quantity == 0 || quantity == sg.Number { - // FIXME: remove fleet & invalidate cache? c.unsafeDeleteShipGroup(sgi) } else { newGroup.Number = quantity @@ -437,6 +436,7 @@ func (c *Cache) ShipGroupBreak(ri int, groupID uuid.UUID, quantity uint) error { if quantity == 0 || quantity == c.ShipGroup(sgi).Number { c.internalShipGroupJoinFleet(sgi, nil) } else { + // TODO: which group stays in fleet? if _, err := c.breakGroup(ri, groupID, quantity); err != nil { return err } diff --git a/internal/controller/ship_group_test.go b/internal/controller/ship_group_test.go index 2567b69..5b5ab6c 100644 --- a/internal/controller/ship_group_test.go +++ b/internal/controller/ship_group_test.go @@ -572,3 +572,20 @@ func TestShipGroupDestroyItem(t *testing.T) { func TestState(t *testing.T) { assert.Equal(t, "In_Orbit", fmt.Sprintf("%s", game.StateInOrbit)) } + +func TestUnsafeDeleteShipGroup(t *testing.T) { + c, g := newCache() + + assert.NoError(t, c.CreateShips(Race_0_idx, Race_0_Gunship, R0_Planet_0_num, 3)) // 0 + assert.NoError(t, c.CreateShips(Race_0_idx, Race_0_Gunship, R0_Planet_2_num, 5)) // 1 + assert.NoError(t, g.ShipGroupJoinFleet(Race_0.Name, "Fleet", c.ShipGroup(0).ID, 0)) + assert.NoError(t, c.CreateShips(Race_0_idx, Race_0_Freighter, R0_Planet_2_num, 7)) // 2 + + assert.Len(t, slices.Collect(c.RaceShipGroups(Race_0_idx)), 3) + + c.UnsafeDeleteShipGroup(1) + + assert.Len(t, slices.Collect(c.RaceShipGroups(Race_0_idx)), 2) + assert.Equal(t, uint(3), c.ShipGroup(0).Number) + assert.Equal(t, uint(7), c.ShipGroup(1).Number) +} diff --git a/internal/model/game/group.go b/internal/model/game/group.go index 226a58a..200aec4 100644 --- a/internal/model/game/group.go +++ b/internal/model/game/group.go @@ -3,6 +3,7 @@ package game import ( "fmt" "math" + "strings" "github.com/google/uuid" ) @@ -17,9 +18,9 @@ const ( var ( CargoTypeSet map[string]CargoType = map[string]CargoType{ - CargoColonist.String(): CargoColonist, - CargoMaterial.String(): CargoMaterial, - CargoCapital.String(): CargoCapital, + strings.ToLower(CargoColonist.String()): CargoColonist, + strings.ToLower(CargoMaterial.String()): CargoMaterial, + strings.ToLower(CargoCapital.String()): CargoCapital, } ) diff --git a/internal/model/game/route.go b/internal/model/game/route.go index e2ed9b9..a8a9b71 100644 --- a/internal/model/game/route.go +++ b/internal/model/game/route.go @@ -1,5 +1,7 @@ package game +import "strings" + type RouteType string const ( @@ -11,10 +13,10 @@ const ( var ( RouteTypeSet map[string]RouteType = map[string]RouteType{ - RouteMaterial.String(): RouteMaterial, - RouteCapital.String(): RouteCapital, - RouteColonist.String(): RouteColonist, - RouteEmpty.String(): RouteEmpty, + strings.ToLower(RouteMaterial.String()): RouteMaterial, + strings.ToLower(RouteCapital.String()): RouteCapital, + strings.ToLower(RouteColonist.String()): RouteColonist, + strings.ToLower(RouteEmpty.String()): RouteEmpty, } RouteToCargo map[RouteType]CargoType = map[RouteType]CargoType{ RouteColonist: CargoColonist, diff --git a/internal/model/rest/command.go b/internal/model/rest/command.go index 1a675a9..4d82d06 100644 --- a/internal/model/rest/command.go +++ b/internal/model/rest/command.go @@ -35,31 +35,162 @@ const ( CommandTypePlanetRouteRemove CommandType = "planetRouteRemove" ) +type DecodableCommand interface { + CommandType() CommandType +} + type CommandMeta struct { Type CommandType `json:"@type" binding:"required,notblank"` } +func (cm CommandMeta) CommandType() CommandType { + return cm.Type +} + type CommandRaceQuit struct { CommandMeta } type CommandRaceVote struct { CommandMeta - Recipient string `json:"recipient" binding:"required,notblank"` + Acceptor string `json:"acceptor" binding:"required,notblank"` } type CommandRaceRelation struct { CommandMeta - Opponent string `json:"recipient" binding:"required,notblank"` + Acceptor string `json:"acceptor" binding:"required,notblank"` Relation string `json:"relation" binding:"required,notblank"` } type CommandShipClassCreate struct { CommandMeta - Name string `json:"name" binding:"required,notblank"` + Name string `json:"name" binding:"required,notblank,entity"` Drive float64 `json:"drive" binding:"eq=0|gte=1"` Armament int `json:"armament" binding:"ammoWeapons=Weapons"` Weapons float64 `json:"weapons" binding:"ammoWeapons=Armament"` Shields float64 `json:"shields" binding:"eq=0|gte=1"` Cargo float64 `json:"cargo" binding:"eq=0|gte=1"` } + +type CommandShipClassMerge struct { + CommandMeta + Name string `json:"name" binding:"required,notblank,entity,nefield=Target"` + Target string `json:"target" binding:"required,notblank,entity,nefield=Class"` +} + +type CommandShipClassRemove struct { + CommandMeta + Name string `json:"name" binding:"required,notblank,entity"` +} + +type CommandShipGroupLoad struct { + CommandMeta + ID string `json:"id" binding:"required,uuid_rfc4122"` + Cargo string `json:"cargo" binding:"required,notblank,oneof=COL MAT CAP"` + Ships int `json:"ships" binding:"gte=0"` + Quantity float64 `json:"quantity" binding:"gte=0"` +} + +type CommandShipGroupUnload struct { + CommandMeta + ID string `json:"id" binding:"required,uuid_rfc4122"` + Ships int `json:"ships" binding:"gte=0"` + Quantity float64 `json:"quantity" binding:"gte=0"` +} + +type CommandShipGroupSend struct { + CommandMeta + ID string `json:"id" binding:"required,uuid_rfc4122"` + Destination int `json:"planetNumber" binding:"gte=0"` + Quantity int `json:"quantity" binding:"gte=0"` +} + +type CommandShipGroupUpgrade struct { + CommandMeta + ID string `json:"id" binding:"required,uuid_rfc4122"` + Tech string `json:"tech" binding:"oneof=ALL DRIVE WEAPONS SHIELDS CARGO"` + MaxShips int `json:"maxShips" binding:"gte=0"` + Level int `json:"level" binding:"gte=1"` +} + +type CommandShipGroupMerge struct { + CommandMeta +} + +type CommandShipGroupBreak struct { + CommandMeta + ID string `json:"id" binding:"required,uuid_rfc4122"` + Quantity int `json:"quantity" binding:"gte=0"` +} + +type CommandShipGroupDismantle struct { + CommandMeta + ID string `json:"id" binding:"required,uuid_rfc4122"` + Quantity int `json:"quantity" binding:"gte=0"` +} + +type CommandShipGroupTransfer struct { + CommandMeta + ID string `json:"id" binding:"required,uuid_rfc4122"` + Acceptor string `json:"acceptor" binding:"required,notblank"` + Quantity int `json:"quantity" binding:"gte=0"` +} + +type CommandShipGroupJoinFleet struct { + CommandMeta + ID string `json:"id" binding:"required,uuid_rfc4122"` + Name string `json:"name" binding:"required,notblank,entity"` + Quantity int `json:"quantity" binding:"gte=0"` +} + +type CommandFleetMerge struct { + CommandMeta + Name string `json:"name" binding:"required,notblank,entity,nefield=Target"` + Target string `json:"target" binding:"required,notblank,entity,nefield=Name"` +} + +type CommandFleetSend struct { + CommandMeta + Name string `json:"name" binding:"required,notblank,entity"` + Destination int `json:"planetNumber" binding:"gte=0"` +} + +type CommandScienceCreate struct { + CommandMeta + Name string `json:"name" binding:"required,notblank,entity"` + Drive float64 `json:"drive" binding:"gte=0,lte=1"` + Weapons float64 `json:"weapons" binding:"gte=0,lte=1"` + Shields float64 `json:"shields" binding:"gte=0,lte=1"` + Cargo float64 `json:"cargo" binding:"gte=0,lte=1"` +} + +type CommandScienceRemove struct { + CommandMeta + Name string `json:"name" binding:"required,notblank,entity"` +} + +type CommandPlanetRename struct { + CommandMeta + Number int `json:"planetNumber" binding:"gte=0"` + Name string `json:"name" binding:"required,notblank,entity"` +} + +type CommandPlanetProduce struct { + CommandMeta + Number int `json:"planetNumber" binding:"gte=0"` + Production string `json:"production" binding:"oneof=MAT CAP DRIVE WEAPONS SHIELDS CARGO SCIENCE SHIP"` + Subject string `json:"subject" binding:"subject"` +} + +type CommandPlanetRouteSet struct { + CommandMeta + Origin int `json:"fromPlanetNumber" binding:"gte=0,nefield=Destination"` + Destination int `json:"toPlanetNumber" binding:"gte=0,nefield=Number"` + LoadType string `json:"loadType" binding:"oneof=MAT CAP COL EMP"` +} + +type CommandPlanetRouteRemove struct { + CommandMeta + Origin int `json:"fromPlanetNumber" binding:"gte=0,nefield=Destination"` + LoadType string `json:"loadType" binding:"oneof=MAT CAP COL EMP"` +} diff --git a/internal/router/command_test.go b/internal/router/command_test.go index 973ec78..12b4ec0 100644 --- a/internal/router/command_test.go +++ b/internal/router/command_test.go @@ -22,7 +22,7 @@ func TestCommand(t *testing.T) { Commands: []json.RawMessage{ encodeCommand(&rest.CommandRaceVote{ CommandMeta: rest.CommandMeta{Type: rest.CommandTypeRaceVote}, - Recipient: "AnotherRace", + Acceptor: "AnotherRace", }), }, } diff --git a/internal/router/handler/command.go b/internal/router/handler/command.go index aa960c6..a72f545 100644 --- a/internal/router/handler/command.go +++ b/internal/router/handler/command.go @@ -63,42 +63,43 @@ func commandRaceQuit(actor string) (Command, error) { } func commandRaceVote(actor string, c json.RawMessage) (Command, error) { - var v rest.CommandRaceVote - if err := json.Unmarshal(c, &v); err != nil { + if v, err := unmarshallCommand(c, new(rest.CommandRaceVote)); err != nil { return nil, err + } else { + return func(c controller.Ctrl) error { + return c.RaceVote(actor, v.Acceptor) + }, nil } - if err := validateCommand(v); err != nil { - return nil, err - } - return func(c controller.Ctrl) error { - return c.RaceVote(actor, v.Recipient) - }, nil } func commandRaceRelation(actor string, c json.RawMessage) (Command, error) { - var v rest.CommandRaceRelation - if err := json.Unmarshal(c, &v); err != nil { + if v, err := unmarshallCommand(c, new(rest.CommandRaceRelation)); err != nil { return nil, err + } else { + return func(c controller.Ctrl) error { + return c.RaceRelation(actor, v.Acceptor, v.Relation) + }, nil } - if err := validateCommand(v); err != nil { - return nil, err - } - return func(c controller.Ctrl) error { - return c.RaceRelation(actor, v.Opponent, v.Relation) - }, nil } func commandShipClassCreate(actor string, c json.RawMessage) (Command, error) { - v := new(rest.CommandShipClassCreate) + if v, err := unmarshallCommand(c, new(rest.CommandShipClassCreate)); err != nil { + return nil, err + } else { + return func(c controller.Ctrl) error { + return c.ShipClassCreate(actor, v.Name, v.Drive, int(v.Armament), v.Weapons, v.Shields, v.Cargo) + }, nil + } +} + +func unmarshallCommand[T rest.DecodableCommand](c json.RawMessage, v *T) (*T, error) { if err := json.Unmarshal(c, v); err != nil { return nil, err } if err := validateCommand(v); err != nil { return nil, err } - return func(c controller.Ctrl) error { - return c.ShipClassCreate(actor, v.Name, v.Drive, int(v.Armament), v.Weapons, v.Shields, v.Cargo) - }, nil + return v, nil } func validateCommand(v any) error { diff --git a/internal/router/router.go b/internal/router/router.go index 556b42f..8eeefc2 100644 --- a/internal/router/router.go +++ b/internal/router/router.go @@ -54,6 +54,12 @@ func setupRouter(executor handler.CommandExecutor) *gin.Engine { if err := v.RegisterValidation("ammoWeapons", armamentWithWeaponsValidator); err != nil { panic(err) } + if err := v.RegisterValidation("entity", entityNameStringValidator); err != nil { + panic(err) + } + if err := v.RegisterValidation("subject", productionTypeStringValidator); err != nil { + panic(err) + } } groupV1 := r.Group("/api/v1") diff --git a/internal/router/validator.go b/internal/router/validator.go index 97a4354..4c10408 100644 --- a/internal/router/validator.go +++ b/internal/router/validator.go @@ -4,6 +4,7 @@ import ( "strings" "github.com/go-playground/validator/v10" + "github.com/iliadenisov/galaxy/internal/util" ) var notBlankStringValidator validator.Func = func(fl validator.FieldLevel) bool { @@ -16,6 +17,27 @@ var notBlankStringValidator validator.Func = func(fl validator.FieldLevel) bool return true } +var entityNameStringValidator validator.Func = func(fl validator.FieldLevel) bool { + s, ok := fl.Field().Interface().(string) + if ok { + if _, ok := util.ValidateTypeName(s); !ok { + return false + } + } + return true +} + +var productionTypeStringValidator validator.Func = func(fl validator.FieldLevel) bool { + v, ok := fl.Field().Interface().(string) + if ok { + f := fl.Parent().FieldByName(fl.Param()) + if s, ok := f.Interface().(string); ok && (s == "SHIP" || s == "SCIENCE") && len(strings.TrimSpace(v)) == 0 { + return false + } + } + return true +} + var armamentWithWeaponsValidator validator.Func = func(fl validator.FieldLevel) bool { var v, compareTo float64 diff --git a/internal/util/string.go b/internal/util/string.go index 388215c..80ac210 100644 --- a/internal/util/string.go +++ b/internal/util/string.go @@ -14,6 +14,7 @@ var allowedSpecialChars = map[rune]bool{ '_': true, } +// TODO: router validator func ValidateTypeName(input string) (string, bool) { // Trim leading and trailing spaces trimmed := strings.TrimSpace(input)