feat: more validators

This commit is contained in:
Ilia Denisov
2026-02-11 00:30:37 +02:00
parent 43dc8ab3f9
commit a477f5ce0b
12 changed files with 224 additions and 39 deletions
+7 -7
View File
@@ -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)
}
@@ -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)
}
+1 -1
View File
@@ -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
}
+17
View File
@@ -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)
}
+4 -3
View File
@@ -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,
}
)
+6 -4
View File
@@ -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,
+134 -3
View File
@@ -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"`
}
+1 -1
View File
@@ -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",
}),
},
}
+21 -20
View File
@@ -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 {
+6
View File
@@ -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")
+22
View File
@@ -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
+1
View File
@@ -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)