diff --git a/internal/controller/command.go b/internal/controller/command.go index 19fcfc8..6fb9610 100644 --- a/internal/controller/command.go +++ b/internal/controller/command.go @@ -26,6 +26,7 @@ import ( "github.com/iliadenisov/galaxy/internal/model/game" ) +// RaceID returns ID of race with given actor's name or error when race not found or extinct func (c Controller) RaceID(actor string) (uuid.UUID, error) { ri, err := c.Cache.validRace(actor) if err != nil { diff --git a/internal/controller/controller.go b/internal/controller/controller.go index 7447c42..0a72b28 100644 --- a/internal/controller/controller.go +++ b/internal/controller/controller.go @@ -5,6 +5,7 @@ import ( "github.com/google/uuid" "github.com/iliadenisov/galaxy/internal/model/game" + "github.com/iliadenisov/galaxy/internal/model/order" "github.com/iliadenisov/galaxy/internal/model/report" "github.com/iliadenisov/galaxy/internal/repo" ) @@ -41,9 +42,17 @@ type Repo interface { // LoadReport loads report for specific turn and player id LoadReport(uint, uuid.UUID) (*report.Report, error) + + // SaveOrder stores order for given turn + SaveOrder(uint, uuid.UUID, *order.Order) error + + // LoadOrder loads order for specific turn and player id + LoadOrder(uint, uuid.UUID) (*order.Order, bool, error) } type Ctrl interface { + ValidateOrder(actor string, cmd ...order.DecodableCommand) error + // remove below funcs if /command api will be deleted RaceID(actor string) (uuid.UUID, error) RaceQuit(actor string) error RaceVote(actor, acceptor string) error @@ -94,7 +103,7 @@ func GenerateTurn(configure func(*Param)) (err error) { if err != nil { return err } - err = ec.ExecuteLocked(func(c *Controller) error { return c.MakeTurn() }) + err = ec.executeLocked(func(c *Controller) error { return c.MakeTurn() }) return } @@ -103,7 +112,15 @@ func ExecuteCommand(configure func(*Param), consumer func(c Ctrl) error) (err er if err != nil { return err } - return ec.ExecuteCommand(func(c *Controller) error { return consumer(c) }) + return ec.executeCommand(func(c *Controller) error { return consumer(c) }) +} + +func ValidateOrder(configure func(*Param), actor string, cmd ...order.DecodableCommand) (err error) { + ec, err := NewRepoController(configure) + if err != nil { + return err + } + return ec.validateOrder(actor, cmd...) } func GameState(configure func(*Param)) (s game.State, err error) { @@ -138,34 +155,6 @@ type RepoController struct { Repo Repo } -func (ec *RepoController) ExecuteLocked(consumer func(*Controller) error) (err error) { - if err := ec.Repo.Lock(); err != nil { - return err - } - defer func() { - err = errors.Join(err, ec.Repo.Release()) - }() - - g, err := ec.Repo.LoadState() - if err != nil { - return err - } - - err = consumer(ec.NewGameController(g)) - return -} - -func (ec *RepoController) ExecuteCommand(consumer func(*Controller) error) (err error) { - return ec.ExecuteLocked(func(c *Controller) error { - err = consumer(c) - if err == nil { - c.Cache.StageCommand() - err = c.saveState() - } - return err - }) -} - func NewRepoController(config Configurer) (*RepoController, error) { c := &Param{ StoragePath: ".", @@ -189,6 +178,60 @@ func (ec *RepoController) NewGameController(g *game.Game) *Controller { } } +func (ec *RepoController) validateOrder(actor string, cmd ...order.DecodableCommand) (err error) { + return ec.executeSafe(func(t uint, c *Controller) error { + id, err := c.RaceID(actor) + if err != nil { + return err + } + err = c.ValidateOrder(actor, cmd...) + if err != nil { + return err + } + o := &order.Order{Commands: make([]order.DecodableCommand, len(cmd))} + copy(o.Commands, cmd) + return ec.Repo.SaveOrder(t, id, o) + }) +} + +func (ec *RepoController) executeCommand(consumer func(*Controller) error) (err error) { + return ec.executeLocked(func(c *Controller) error { + err = consumer(c) + if err == nil { + c.Cache.StageCommand() + err = c.saveState() + } + return err + }) +} + +func (ec *RepoController) executeSafe(consumer func(uint, *Controller) error) (err error) { + g, err := ec.Repo.LoadStateSafe() + if err != nil { + return err + } + + err = consumer(g.Turn, ec.NewGameController(g)) + return +} + +func (ec *RepoController) executeLocked(consumer func(*Controller) error) (err error) { + if err := ec.Repo.Lock(); err != nil { + return err + } + defer func() { + err = errors.Join(err, ec.Repo.Release()) + }() + + g, err := ec.Repo.LoadState() + if err != nil { + return err + } + + err = consumer(ec.NewGameController(g)) + return +} + func (c *Controller) saveState() error { return c.Repo.SaveLastState(c.Cache.g) } diff --git a/internal/controller/generate_turn.go b/internal/controller/generate_turn.go index d6b0f73..6bd72ac 100644 --- a/internal/controller/generate_turn.go +++ b/internal/controller/generate_turn.go @@ -10,12 +10,15 @@ import ( ) func (c *Controller) MakeTurn() error { + + if err := c.applyOrders(c.Cache.g.Turn); err != nil { + return err + } + // Next turn c.Cache.g.Turn += 1 c.Cache.g.Stage = 0 - // TODO: Выполнение приказов - // 01. Вышедшие расы удаляются из списка участвующих рас перед началом просчета очередного хода c.Cache.TurnWipeExtinctRaces() diff --git a/internal/controller/order.go b/internal/controller/order.go new file mode 100644 index 0000000..84dfd07 --- /dev/null +++ b/internal/controller/order.go @@ -0,0 +1,160 @@ +package controller + +import ( + "errors" + "fmt" + + "github.com/google/uuid" + e "github.com/iliadenisov/galaxy/internal/error" + "github.com/iliadenisov/galaxy/internal/model/order" +) + +func (c *Controller) ValidateOrder(actor string, commands ...order.DecodableCommand) (err error) { + for i := range commands { + if _, ok := commands[i].(order.CommandRaceQuit); ok && i != len(commands)-1 { + err = e.NewQuitCommandFollowedByCommandError() + } + if err != nil { + return err + } + err = errors.Join(err, c.applyCommand(actor, commands[i])) + } + return +} + +func (c *Controller) applyCommand(actor string, cmd order.DecodableCommand) (err error) { + var m *order.CommandMeta + if v, ok := order.AsCommand[*order.CommandRaceQuit](cmd); ok { + m = &v.CommandMeta + err = c.RaceQuit(actor) + } else if v, ok := order.AsCommand[*order.CommandRaceVote](cmd); ok { + m = &v.CommandMeta + err = c.RaceVote(actor, v.Acceptor) + } else if v, ok := order.AsCommand[*order.CommandRaceRelation](cmd); ok { + m = &v.CommandMeta + err = c.RaceRelation(actor, v.Acceptor, v.Relation) + } else if v, ok := order.AsCommand[*order.CommandShipClassCreate](cmd); ok { + m = &v.CommandMeta + err = c.ShipClassCreate(actor, v.Name, v.Drive, int(v.Armament), v.Weapons, v.Shields, v.Cargo) + } else if v, ok := order.AsCommand[*order.CommandShipClassMerge](cmd); ok { + m = &v.CommandMeta + err = c.ShipClassMerge(actor, v.Name, v.Target) + } else if v, ok := order.AsCommand[*order.CommandShipClassRemove](cmd); ok { + m = &v.CommandMeta + err = c.ShipClassRemove(actor, v.Name) + } else if v, ok := order.AsCommand[*order.CommandShipGroupLoad](cmd); ok { + m = &v.CommandMeta + err = c.ShipGroupLoad(actor, uuid.MustParse(v.ID), v.Cargo, v.Quantity) + } else if v, ok := order.AsCommand[*order.CommandShipGroupUnload](cmd); ok { + m = &v.CommandMeta + err = c.ShipGroupUnload(actor, uuid.MustParse(v.ID), v.Quantity) + } else if v, ok := order.AsCommand[*order.CommandShipGroupSend](cmd); ok { + m = &v.CommandMeta + err = c.ShipGroupSend(actor, uuid.MustParse(v.ID), uint(v.Destination)) + } else if v, ok := order.AsCommand[*order.CommandShipGroupUpgrade](cmd); ok { + m = &v.CommandMeta + err = c.ShipGroupUpgrade(actor, uuid.MustParse(v.ID), v.Tech, v.Level) + } else if v, ok := order.AsCommand[*order.CommandShipGroupMerge](cmd); ok { + m = &v.CommandMeta + err = c.ShipGroupMerge(actor) + } else if v, ok := order.AsCommand[*order.CommandShipGroupBreak](cmd); ok { + m = &v.CommandMeta + err = c.ShipGroupBreak(actor, uuid.MustParse(v.ID), uuid.MustParse(v.NewID), uint(v.Quantity)) + } else if v, ok := order.AsCommand[*order.CommandShipGroupDismantle](cmd); ok { + m = &v.CommandMeta + err = c.ShipGroupDismantle(actor, uuid.MustParse(v.ID)) + } else if v, ok := order.AsCommand[*order.CommandShipGroupTransfer](cmd); ok { + m = &v.CommandMeta + err = c.ShipGroupTransfer(actor, v.Acceptor, uuid.MustParse(v.ID)) + } else if v, ok := order.AsCommand[*order.CommandShipGroupJoinFleet](cmd); ok { + m = &v.CommandMeta + err = c.ShipGroupJoinFleet(actor, v.Name, uuid.MustParse(v.ID)) + } else if v, ok := order.AsCommand[*order.CommandFleetMerge](cmd); ok { + m = &v.CommandMeta + err = c.FleetMerge(actor, v.Name, v.Target) + } else if v, ok := order.AsCommand[*order.CommandFleetSend](cmd); ok { + m = &v.CommandMeta + err = c.FleetSend(actor, v.Name, uint(v.Destination)) + } else if v, ok := order.AsCommand[*order.CommandScienceCreate](cmd); ok { + m = &v.CommandMeta + err = c.ScienceCreate(actor, v.Name, v.Drive, v.Weapons, v.Shields, v.Cargo) + } else if v, ok := order.AsCommand[*order.CommandScienceRemove](cmd); ok { + m = &v.CommandMeta + err = c.ScienceRemove(actor, v.Name) + } else if v, ok := order.AsCommand[*order.CommandPlanetRename](cmd); ok { + m = &v.CommandMeta + err = c.PlanetRename(actor, v.Number, v.Name) + } else if v, ok := order.AsCommand[*order.CommandPlanetProduce](cmd); ok { + m = &v.CommandMeta + err = c.PlanetProduce(actor, v.Number, v.Production, v.Subject) + } else if v, ok := order.AsCommand[*order.CommandPlanetRouteSet](cmd); ok { + m = &v.CommandMeta + err = c.PlanetRouteSet(actor, v.LoadType, uint(v.Origin), uint(v.Destination)) + } else if v, ok := order.AsCommand[*order.CommandPlanetRouteRemove](cmd); ok { + m = &v.CommandMeta + err = c.PlanetRouteRemove(actor, v.LoadType, uint(v.Origin)) + } else { + return e.NewUnrecognizedCommandError(cmd.CommandType().String()) + } + + if ge, ok := errors.AsType[*e.GenericError](err); ok { + m.Result(ge.Code) + } else if err != nil { + panic(fmt.Errorf("error applying command has unknown origin: %w", err)) + } else { + m.Result(0) + } + + return +} + +// TODO: test commands ordering +func (c *Controller) applyOrders(t uint) error { + raceOrder := make(map[int][]order.DecodableCommand) + unloadGroups := make([]uuid.UUID, 0) + unloadQuantities := make([]float64, 0) + for ri := range c.Cache.listRaceActingIdx() { + o, ok, err := c.Repo.LoadOrder(t, c.Cache.g.Race[ri].ID) + if err != nil { + return err + } + if !ok { + continue + } + raceOrder[ri] = o.Commands + for i := range o.Commands { + if o.Commands[i].CommandType() == order.CommandTypeShipGroupUnload { + unloadCommand := o.Commands[i].(order.CommandShipGroupUnload) + unloadGroups = append(unloadGroups, uuid.MustParse(unloadCommand.ID)) + unloadQuantities = append(unloadQuantities, unloadCommand.Quantity) + } + } + } + + if err := c.Cache.shipGroupUnloadColonistChallenge(unloadGroups, unloadQuantities); err != nil { + return err + } + + for ri := range raceOrder { + for _, cmd := range raceOrder[ri] { + if cmd.CommandType() == order.CommandTypeShipGroupUnload { + // group unload commands execution should be failed at this point, + // otherwise produce no-errors + if v, ok := order.AsCommand[*order.CommandShipGroupUnload](cmd); ok { + v.Result(0) + } + continue + } + // any command might fail due to challenged planets colonization + _ = c.applyCommand(c.Cache.g.Race[ri].Name, cmd) + } + } + + for ri := range c.Cache.listRaceActingIdx() { + if err := c.Repo.SaveOrder(t, c.Cache.g.Race[ri].ID, &order.Order{Commands: raceOrder[ri]}); err != nil { + return err + } + } + + return nil +} diff --git a/internal/controller/race.go b/internal/controller/race.go index b25f87f..8f3bb76 100644 --- a/internal/controller/race.go +++ b/internal/controller/race.go @@ -91,6 +91,7 @@ func (c *Cache) validActor(name string) (int, error) { return i, nil } +// validRace returns index of race with given name or error when race not found or extinct func (c *Cache) validRace(name string) (int, error) { i, err := c.raceIndex(name) if err != nil { diff --git a/internal/controller/route.go b/internal/controller/route.go index ff08054..e289ea6 100644 --- a/internal/controller/route.go +++ b/internal/controller/route.go @@ -277,17 +277,21 @@ func (c *Cache) listRoutedUnloadShipGroupIds(pn uint, routeType game.RouteType) } } -func MaxOrRandomLoadId(IDtoLoad map[int]float64) int { - if len(IDtoLoad) < 2 { - panic("IDtoLoad must contain at least 2 keys") +func MaxOrRandomLoadId(loadByRace map[int]float64) int { + if len(loadByRace) < 2 { + panic("loadByRace must contain at least 2 keys") } - IDs := slices.Collect(maps.Keys(IDtoLoad)) - slices.SortFunc(IDs, func(id1, id2 int) int { return cmp.Compare(IDtoLoad[id2], IDtoLoad[id1]) }) + IDs := slices.Collect(maps.Keys(loadByRace)) + slices.SortFunc(IDs, func(id1, id2 int) int { + return cmp.Or( + cmp.Compare(loadByRace[id2], loadByRace[id1]), + ) + }) // no single winner with highest load - if IDtoLoad[IDs[0]] == IDtoLoad[IDs[1]] { + if loadByRace[IDs[0]] == loadByRace[IDs[1]] { // remove IDs which load less than maximum - IDs = slices.DeleteFunc(IDs, func(v int) bool { return IDtoLoad[v] < IDtoLoad[IDs[0]] }) + IDs = slices.DeleteFunc(IDs, func(v int) bool { return loadByRace[v] < loadByRace[IDs[0]] }) // IDs[0] will have random index rand.Shuffle(len(IDs), func(i, j int) { IDs[i], IDs[j] = IDs[j], IDs[i] }) } diff --git a/internal/controller/ship_group.go b/internal/controller/ship_group.go index a685b1d..27abc9c 100644 --- a/internal/controller/ship_group.go +++ b/internal/controller/ship_group.go @@ -5,6 +5,7 @@ import ( "fmt" "iter" "maps" + "math/rand/v2" "slices" "github.com/google/uuid" @@ -295,6 +296,95 @@ func (c *Cache) shipGroupUnload(ri int, groupID uuid.UUID, quantity float64) err return nil } +func (c *Cache) shipGroupUnloadColonistChallenge(groupIDs []uuid.UUID, quantites []float64) error { + if len(groupIDs) != len(quantites) { + e.NewGameStateError("challenge group unload: groups=%d quantities=%d", len(groupIDs), len(quantites)) + } + if len(groupIDs) == 0 { + return nil + } + type challenger struct { + ri int + sgi int + quantity float64 + } + challenge := make(map[uint]map[int][]challenger, 0) + for i, gid := range groupIDs { + sgi, ok := c.shipGroupIndexByID(gid) + if !ok { + return e.NewGameStateError("challenge group unload: group not found: %v", gid) + } + sg := c.ShipGroup(sgi) + pn, ok := sg.AtPlanet() + if !ok || sg.CargoType == nil { + continue + } + p := c.MustPlanet(pn) + ri := c.ShipGroupOwnerRaceIndex(sgi) + if p.Owned() || *sg.CargoType != game.CargoColonist { + if err := c.shipGroupUnload(ri, gid, quantites[i]); err != nil { + return err + } + } + if _, ok := challenge[pn]; !ok { + challenge[pn] = make(map[int][]challenger) + } + challenge[pn][ri] = append(challenge[pn][ri], challenger{ri: ri, sgi: sgi, quantity: quantites[i]}) + } + for pn := range challenge { + if len(challenge[pn]) < 2 { + for _, v := range challenge[pn] { + for _, ch := range v { + if err := c.shipGroupUnload(ch.ri, c.ShipGroup(ch.sgi).ID, ch.quantity); err != nil { + return err + } + } + } + continue + } + sum := make(map[int]float64) + races := slices.Collect(maps.Keys(challenge[pn])) + for ri, groups := range challenge[pn] { + for i := range groups { + sum[ri] = sum[ri] + groups[i].quantity + } + c.listProducingPlanets() + } + + slices.SortFunc(races, func(ria, rib int) int { + return cmp.Or( + // Наибольшее количество выгружаемых колонистов + cmp.Compare(sum[rib], sum[ria]), + + // Наибольшее количество населения расы (они же голоса) + cmp.Compare(c.g.Race[rib].Votes, c.g.Race[ria].Votes), + + // Случайный выбор претендента + cmp.Compare(rand.Float64(), rand.Float64()), + + // in theoty, unreacheable option, but let's randomize again + cmp.Compare(rand.Float64(), rand.Float64()), + ) + }) + + for _, ch := range challenge[pn][races[0]] { + if err := c.shipGroupUnload(ch.ri, c.ShipGroup(ch.sgi).ID, ch.quantity); err != nil { + return err + } + } + } + return nil +} + +func (c *Cache) shipGroupIndexByID(id uuid.UUID) (int, bool) { + for sgi := range c.g.ShipGroups { + if c.g.ShipGroups[sgi].ID == id { + return sgi, true + } + } + return -1, false +} + func (c *Cache) unsafeUnloadCargo(sgi int, q float64) { if q <= 0 { return diff --git a/internal/error/generic.go b/internal/error/generic.go index bf4f515..e3688ad 100644 --- a/internal/error/generic.go +++ b/internal/error/generic.go @@ -63,6 +63,8 @@ const ( ErrInputUpgradeParameterNotAllowed ErrInputUpgradeShipsAlreadyUpToDate ErrInputUpgradeTechLevelInsufficient + ErrInputQuitCommandFollowedByCommand + ErrInputUnrecognizedCommand ) func GenericErrorText(code int) string { @@ -159,6 +161,10 @@ func GenericErrorText(code int) string { return "Insufficient planet production capacity" case ErrInputUpgradeTechLevelInsufficient: return "Insifficient Tech level for requested upgrade" + case ErrInputQuitCommandFollowedByCommand: + return "'Quit' must be the last order's command" + case ErrInputUnrecognizedCommand: + return "Unrecognized command" case ErrSendShipHasNoDrives: return "One or more ships are not equipped with hyperdrive and cannot be moved" case ErrSendUnreachableDestination: diff --git a/internal/error/input.go b/internal/error/input.go index ce083d5..725e3c2 100644 --- a/internal/error/input.go +++ b/internal/error/input.go @@ -167,3 +167,11 @@ func NewSendUnreachableDestinationError(arg ...any) error { func NewSendShipOwnerHasNoPlanetsError(arg ...any) error { return newGenericError(ErrSendShipOwnerHasNoPlanets, arg...) } + +func NewQuitCommandFollowedByCommandError(arg ...any) error { + return newGenericError(ErrInputQuitCommandFollowedByCommand, arg...) +} + +func NewUnrecognizedCommandError(arg ...any) error { + return newGenericError(ErrInputUnrecognizedCommand, arg...) +} diff --git a/internal/model/order/order.go b/internal/model/order/order.go new file mode 100644 index 0000000..c7dae83 --- /dev/null +++ b/internal/model/order/order.go @@ -0,0 +1,218 @@ +package order + +import ( + "encoding/json" +) + +type Order struct { + Commands []DecodableCommand `json:"cmd"` +} + +func (o Order) MarshalBinary() (data []byte, err error) { + return json.Marshal(&o) +} + +func (o *Order) UnmarshalBinary(data []byte) error { + return json.Unmarshal(data, o) +} + +func AsCommand[E DecodableCommand](c DecodableCommand) (E, bool) { + if v, ok := c.(E); ok { + return v, true + } + return *new(E), false +} + +type CommandType string + +const ( + CommandTypeRaceQuit CommandType = "raceQuit" + CommandTypeRaceVote CommandType = "raceVote" + CommandTypeRaceRelation CommandType = "raceRelation" + CommandTypeShipClassCreate CommandType = "shipClassCreate" + CommandTypeShipClassMerge CommandType = "shipClassMerge" + CommandTypeShipClassRemove CommandType = "shipClassRemove" + CommandTypeShipGroupBreak CommandType = "shipGroupBreak" + CommandTypeShipGroupLoad CommandType = "shipGroupLoad" + CommandTypeShipGroupUnload CommandType = "shipGroupUnload" + CommandTypeShipGroupSend CommandType = "shipGroupSend" + CommandTypeShipGroupUpgrade CommandType = "shipGroupUpgrade" + CommandTypeShipGroupMerge CommandType = "shipGroupMerge" + CommandTypeShipGroupDismantle CommandType = "shipGroupDismantle" + CommandTypeShipGroupTransfer CommandType = "shipGroupTransfer" + CommandTypeShipGroupJoinFleet CommandType = "shipGroupJoinFleet" + CommandTypeFleetMerge CommandType = "fleetMerge" + CommandTypeFleetSend CommandType = "fleetSend" + CommandTypeScienceCreate CommandType = "scienceCreate" + CommandTypeScienceRemove CommandType = "scienceRemove" + CommandTypePlanetRename CommandType = "planetRename" + CommandTypePlanetProduce CommandType = "planetProduce" + CommandTypePlanetRouteSet CommandType = "planetRouteSet" + CommandTypePlanetRouteRemove CommandType = "planetRouteRemove" +) + +func (ct CommandType) String() string { + return string(ct) +} + +type DecodableCommand interface { + CommandType() CommandType +} + +type CommandMeta struct { + CmdType CommandType `json:"@type" binding:"notblank"` + CmdID string `json:"cmdId" binding:"required,uuid_rfc4122"` + CmdApplied *bool `json:"cmdApplied,omitempty"` + CmdErrCode *int `json:"cmdErrorCode,omitempty"` +} + +func (cm CommandMeta) CommandType() CommandType { + return cm.CmdType +} + +func (cm *CommandMeta) Result(errCode int) { + cm.CmdErrCode = &errCode + cm.CmdApplied = new(bool(errCode == 0)) +} + +type CommandRaceQuit struct { + CommandMeta +} + +type CommandRaceVote struct { + CommandMeta + Acceptor string `json:"acceptor" binding:"notblank,entity"` +} + +type CommandRaceRelation struct { + CommandMeta + Acceptor string `json:"acceptor" binding:"notblank,entity"` + Relation string `json:"relation" binding:"oneof=WAR PEACE"` +} + +type CommandShipClassCreate struct { + CommandMeta + Name string `json:"name" binding:"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:"notblank,entity,nefield=Target"` + Target string `json:"target" binding:"notblank,entity,nefield=Name"` +} + +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:"oneof=COL MAT CAP"` + Quantity float64 `json:"quantity" binding:"gte=0"` +} + +type CommandShipGroupUnload struct { + CommandMeta + ID string `json:"id" binding:"required,uuid_rfc4122"` + 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"` +} + +type CommandShipGroupUpgrade struct { + CommandMeta + ID string `json:"id" binding:"required,uuid_rfc4122"` + Tech string `json:"tech" binding:"oneof=ALL DRIVE WEAPONS SHIELDS CARGO"` + Level float64 `json:"level" binding:"eq=0|gt=1"` +} + +type CommandShipGroupMerge struct { + CommandMeta +} + +type CommandShipGroupBreak struct { + CommandMeta + ID string `json:"id" binding:"uuid_rfc4122,nefield=NewID"` + NewID string `json:"newId" binding:"uuid_rfc4122,nefield=ID"` + Quantity int `json:"quantity" binding:"gte=0"` +} + +type CommandShipGroupDismantle struct { + CommandMeta + ID string `json:"id" binding:"required,uuid_rfc4122"` +} + +type CommandShipGroupTransfer struct { + CommandMeta + ID string `json:"id" binding:"required,uuid_rfc4122"` + Acceptor string `json:"acceptor" binding:"required,notblank,entity"` +} + +type CommandShipGroupJoinFleet struct { + CommandMeta + ID string `json:"id" binding:"required,uuid_rfc4122"` + Name string `json:"name" binding:"required,notblank,entity"` +} + +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=Production"` +} + +type CommandPlanetRouteSet struct { + CommandMeta + Origin int `json:"fromPlanetNumber" binding:"gte=0,nefield=Destination"` + Destination int `json:"toPlanetNumber" binding:"gte=0,nefield=Origin"` + LoadType string `json:"loadType" binding:"oneof=MAT CAP COL EMP"` +} + +type CommandPlanetRouteRemove struct { + CommandMeta + Origin int `json:"fromPlanetNumber" binding:"gte=0"` + LoadType string `json:"loadType" binding:"oneof=MAT CAP COL EMP"` +} diff --git a/internal/model/rest/command.go b/internal/model/rest/command.go index fcf380a..04f8c8b 100644 --- a/internal/model/rest/command.go +++ b/internal/model/rest/command.go @@ -7,184 +7,10 @@ type Command struct { Commands []json.RawMessage `json:"cmd" binding:"min=1"` } -type CommandType string - -const ( - CommandTypeRaceQuit CommandType = "raceQuit" - CommandTypeRaceVote CommandType = "raceVote" - CommandTypeRaceRelation CommandType = "raceRelation" - CommandTypeShipClassCreate CommandType = "shipClassCreate" - CommandTypeShipClassMerge CommandType = "shipClassMerge" - CommandTypeShipClassRemove CommandType = "shipClassRemove" - CommandTypeShipGroupBreak CommandType = "shipGroupBreak" - CommandTypeShipGroupLoad CommandType = "shipGroupLoad" - CommandTypeShipGroupUnload CommandType = "shipGroupUnload" - CommandTypeShipGroupSend CommandType = "shipGroupSend" - CommandTypeShipGroupUpgrade CommandType = "shipGroupUpgrade" - CommandTypeShipGroupMerge CommandType = "shipGroupMerge" - CommandTypeShipGroupDismantle CommandType = "shipGroupDismantle" - CommandTypeShipGroupTransfer CommandType = "shipGroupTransfer" - CommandTypeShipGroupJoinFleet CommandType = "shipGroupJoinFleet" - CommandTypeFleetMerge CommandType = "fleetMerge" - CommandTypeFleetSend CommandType = "fleetSend" - CommandTypeScienceCreate CommandType = "scienceCreate" - CommandTypeScienceRemove CommandType = "scienceRemove" - CommandTypePlanetRename CommandType = "planetRename" - CommandTypePlanetProduce CommandType = "planetProduce" - CommandTypePlanetRouteSet CommandType = "planetRouteSet" - CommandTypePlanetRouteRemove CommandType = "planetRouteRemove" -) - -type DecodableCommand interface { - CommandType() CommandType +func (o Command) MarshalBinary() (data []byte, err error) { + return json.Marshal(&o) } -type CommandMeta struct { - Type CommandType `json:"@type" binding:"notblank"` -} - -func (cm CommandMeta) CommandType() CommandType { - return cm.Type -} - -type CommandRaceQuit struct { - CommandMeta -} - -type CommandRaceVote struct { - CommandMeta - Acceptor string `json:"acceptor" binding:"notblank,entity"` -} - -type CommandRaceRelation struct { - CommandMeta - Acceptor string `json:"acceptor" binding:"notblank,entity"` - Relation string `json:"relation" binding:"oneof=WAR PEACE"` -} - -type CommandShipClassCreate struct { - CommandMeta - Name string `json:"name" binding:"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:"notblank,entity,nefield=Target"` - Target string `json:"target" binding:"notblank,entity,nefield=Name"` -} - -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:"oneof=COL MAT CAP"` - Quantity float64 `json:"quantity" binding:"gte=0"` -} - -type CommandShipGroupUnload struct { - CommandMeta - ID string `json:"id" binding:"required,uuid_rfc4122"` - 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"` -} - -type CommandShipGroupUpgrade struct { - CommandMeta - ID string `json:"id" binding:"required,uuid_rfc4122"` - Tech string `json:"tech" binding:"oneof=ALL DRIVE WEAPONS SHIELDS CARGO"` - Level float64 `json:"level" binding:"eq=0|gt=1"` -} - -type CommandShipGroupMerge struct { - CommandMeta -} - -type CommandShipGroupBreak struct { - CommandMeta - ID string `json:"id" binding:"uuid_rfc4122,nefield=NewID"` - NewID string `json:"newId" binding:"uuid_rfc4122,nefield=ID"` - Quantity int `json:"quantity" binding:"gte=0"` -} - -type CommandShipGroupDismantle struct { - CommandMeta - ID string `json:"id" binding:"required,uuid_rfc4122"` -} - -type CommandShipGroupTransfer struct { - CommandMeta - ID string `json:"id" binding:"required,uuid_rfc4122"` - Acceptor string `json:"acceptor" binding:"required,notblank,entity"` -} - -type CommandShipGroupJoinFleet struct { - CommandMeta - ID string `json:"id" binding:"required,uuid_rfc4122"` - Name string `json:"name" binding:"required,notblank,entity"` -} - -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=Production"` -} - -type CommandPlanetRouteSet struct { - CommandMeta - Origin int `json:"fromPlanetNumber" binding:"gte=0,nefield=Destination"` - Destination int `json:"toPlanetNumber" binding:"gte=0,nefield=Origin"` - LoadType string `json:"loadType" binding:"oneof=MAT CAP COL EMP"` -} - -type CommandPlanetRouteRemove struct { - CommandMeta - Origin int `json:"fromPlanetNumber" binding:"gte=0"` - LoadType string `json:"loadType" binding:"oneof=MAT CAP COL EMP"` +func (o *Command) UnmarshalBinary(data []byte) error { + return json.Unmarshal(data, o) } diff --git a/internal/repo/fs/fs.go b/internal/repo/fs/fs.go index f392325..d809fc7 100644 --- a/internal/repo/fs/fs.go +++ b/internal/repo/fs/fs.go @@ -79,15 +79,11 @@ func (f *fs) Exists(path string) (bool, error) { return fileExists(filepath.Join(f.root, path)) } -func (f *fs) Write(path string, v encoding.BinaryMarshaler) error { +func (f *fs) WriteSafe(path string, v encoding.BinaryMarshaler) error { if v == nil { return errors.New("cant't marshal from nil object") } - if f.lock == nil { - return errors.New("lock must be acquired before write") - } - targetFilePath := filepath.Join(f.root, path) if targetFilePath == f.lockFilePath() { return errors.New("can't write to the lock file") @@ -160,6 +156,14 @@ func (f *fs) Write(path string, v encoding.BinaryMarshaler) error { return nil } +func (f *fs) Write(path string, v encoding.BinaryMarshaler) error { + if f.lock == nil { + return errors.New("lock must be acquired before write") + } + + return f.WriteSafe(path, v) +} + func (f *fs) Read(path string, v encoding.BinaryUnmarshaler) error { if f.lock == nil { return errors.New("lock must be acquired before read") @@ -183,7 +187,7 @@ func (f *fs) ReadSafe(path string, v encoding.BinaryUnmarshaler) error { } case <-timeout.C: checker.Stop() - return errors.New("lock acquired, timeout waiting for release") + return errors.New("timeout waiting for lock release") } } } diff --git a/internal/repo/game.go b/internal/repo/game.go index f55cabe..8504914 100644 --- a/internal/repo/game.go +++ b/internal/repo/game.go @@ -4,16 +4,20 @@ package repo /state.json /0001/state.json /0001/meta.json + /0000/order/{UUID}.json /0001/bombing.json /0001/battle/{UUID}.json /0001/report/{UUID}.json */ import ( + "encoding/json" + "errors" "fmt" "github.com/google/uuid" "github.com/iliadenisov/galaxy/internal/model/game" + "github.com/iliadenisov/galaxy/internal/model/order" "github.com/iliadenisov/galaxy/internal/model/report" ) @@ -22,36 +26,16 @@ const ( metaPath = "meta.json" ) -func (r *repo) SaveReport(t uint, rep *report.Report) error { - return saveReport(r.s, t, rep) +type storedOrder struct { + Commands []json.RawMessage `json:"cmd"` } -func saveReport(s Storage, t uint, v *report.Report) error { - path := repDir(t, v.RaceID) - if err := s.Write(path, v); err != nil { - return NewStorageError(err) - } - return nil +func (o storedOrder) MarshalBinary() (data []byte, err error) { + return json.Marshal(&o) } -func (r *repo) LoadReport(t uint, id uuid.UUID) (*report.Report, error) { - return loadReport(r.s, t, id) -} - -func loadReport(s Storage, t uint, id uuid.UUID) (*report.Report, error) { - path := repDir(t, id) - result := new(report.Report) - exist, err := s.Exists(path) - if err != nil { - return nil, NewStorageError(err) - } - if !exist { - return nil, NewReportNotFoundError() - } - if err := s.ReadSafe(path, result); err != nil { - return nil, NewStorageError(err) - } - return result, nil +func (o *storedOrder) UnmarshalBinary(data []byte) error { + return json.Unmarshal(data, o) } func (r *repo) SaveNewTurn(t uint, g *game.Game) error { @@ -59,7 +43,7 @@ func (r *repo) SaveNewTurn(t uint, g *game.Game) error { } func saveNewTurn(s Storage, t uint, g *game.Game) error { - path := fmt.Sprintf("%s/state.json", turnDir(t)) + path := fmt.Sprintf("%s/state.json", TurnDir(t)) exist, err := s.Exists(path) if err != nil { return NewStorageError(err) @@ -132,7 +116,7 @@ func loadMeta(s Storage) (*game.GameMeta, error) { func saveMeta(s Storage, t uint, gm *game.GameMeta) error { // save turn's meta - path := fmt.Sprintf("%s/%s", turnDir(t), metaPath) + path := fmt.Sprintf("%s/%s", TurnDir(t), metaPath) if err := s.Write(path, gm); err != nil { return NewStorageError(err) } @@ -158,7 +142,7 @@ func (r *repo) SaveBattle(t uint, b *report.BattleReport, m *game.BattleMeta) er } func saveBattle(s Storage, t uint, b *report.BattleReport) error { - path := fmt.Sprintf("%s/battle/%s.json", turnDir(t), b.ID.String()) + path := fmt.Sprintf("%s/battle/%s.json", TurnDir(t), b.ID.String()) exist, err := s.Exists(path) if err != nil { return NewStorageError(err) @@ -183,10 +167,173 @@ func (r *repo) SaveBombings(t uint, b []*game.Bombing) error { return saveMeta(r.s, t, meta) } -func repDir(t uint, id uuid.UUID) string { - return fmt.Sprintf("%s/report/%s.json", turnDir(t), id.String()) +func (r *repo) SaveReport(t uint, rep *report.Report) error { + return saveReport(r.s, t, rep) } -func turnDir(t uint) string { +func saveReport(s Storage, t uint, v *report.Report) error { + path := ReportDir(t, v.RaceID) + if err := s.Write(path, v); err != nil { + return NewStorageError(err) + } + return nil +} + +func (r *repo) LoadReport(t uint, id uuid.UUID) (*report.Report, error) { + return loadReport(r.s, t, id) +} + +func loadReport(s Storage, t uint, id uuid.UUID) (*report.Report, error) { + path := ReportDir(t, id) + result := new(report.Report) + exist, err := s.Exists(path) + if err != nil { + return nil, NewStorageError(err) + } + if !exist { + return nil, NewReportNotFoundError() + } + if err := s.ReadSafe(path, result); err != nil { + return nil, NewStorageError(err) + } + return result, nil +} + +func (r *repo) SaveOrder(t uint, id uuid.UUID, o *order.Order) error { + return saveOrder(r.s, t, id, o) +} + +func saveOrder(s Storage, t uint, id uuid.UUID, o *order.Order) error { + path := OrderDir(t, id) + if err := s.WriteSafe(path, o); err != nil { + return NewStorageError(err) + } + return nil +} + +func (r *repo) LoadOrder(t uint, id uuid.UUID) (*order.Order, bool, error) { + return loadOrder(r.s, t, id) +} + +func loadOrder(s Storage, t uint, id uuid.UUID) (*order.Order, bool, error) { + path := OrderDir(t, id) + + exist, err := s.Exists(path) + if err != nil { + return nil, false, NewStorageError(err) + } + if !exist { + return nil, false, nil + } + + cmd := new(storedOrder) + if err := s.ReadSafe(path, cmd); err != nil { + return nil, false, NewStorageError(err) + } + result := &order.Order{Commands: make([]order.DecodableCommand, len(cmd.Commands))} + if len(cmd.Commands) == 0 { + return nil, false, errors.New("no commands were stored") + } + + for i := range cmd.Commands { + command, err := ParseOrder(cmd.Commands[i], nil) + if err != nil { + return nil, false, err + } + result.Commands[i] = command + } + + return result, true, nil +} + +// Helper funcs + +func OrderDir(t uint, id uuid.UUID) string { + return fmt.Sprintf("%s/order/%s.json", TurnDir(t), id.String()) +} + +func ReportDir(t uint, id uuid.UUID) string { + return fmt.Sprintf("%s/report/%s.json", TurnDir(t), id.String()) +} + +func TurnDir(t uint) string { return fmt.Sprintf("%04d", t) } + +func ParseOrder(c json.RawMessage, validator func(order.DecodableCommand) error) (order.DecodableCommand, error) { + meta := new(order.CommandMeta) + if err := json.Unmarshal(c, meta); err != nil { + return nil, err + } + switch t := meta.CmdType; t { + case order.CommandTypeRaceQuit: + return decodeCommand(c, new(order.CommandRaceQuit), validator) + case order.CommandTypeRaceVote: + return decodeCommand(c, new(order.CommandRaceVote), validator) + case order.CommandTypeRaceRelation: + return decodeCommand(c, new(order.CommandRaceRelation), validator) + case order.CommandTypeShipClassCreate: + return decodeCommand(c, new(order.CommandShipClassCreate), validator) + case order.CommandTypeShipClassMerge: + return decodeCommand(c, new(order.CommandShipClassMerge), validator) + case order.CommandTypeShipClassRemove: + return decodeCommand(c, new(order.CommandShipClassRemove), validator) + case order.CommandTypeShipGroupBreak: + return decodeCommand(c, new(order.CommandShipGroupBreak), validator) + case order.CommandTypeShipGroupLoad: + return decodeCommand(c, new(order.CommandShipGroupLoad), validator) + case order.CommandTypeShipGroupUnload: + return decodeCommand(c, new(order.CommandShipGroupUnload), validator) + case order.CommandTypeShipGroupSend: + return decodeCommand(c, new(order.CommandShipGroupSend), validator) + case order.CommandTypeShipGroupUpgrade: + return decodeCommand(c, new(order.CommandShipGroupUpgrade), validator) + case order.CommandTypeShipGroupMerge: + return decodeCommand(c, new(order.CommandShipGroupMerge), validator) + case order.CommandTypeShipGroupDismantle: + return decodeCommand(c, new(order.CommandShipGroupDismantle), validator) + case order.CommandTypeShipGroupTransfer: + return decodeCommand(c, new(order.CommandShipGroupTransfer), validator) + case order.CommandTypeShipGroupJoinFleet: + return decodeCommand(c, new(order.CommandShipGroupJoinFleet), validator) + case order.CommandTypeFleetMerge: + return decodeCommand(c, new(order.CommandFleetMerge), validator) + case order.CommandTypeFleetSend: + return decodeCommand(c, new(order.CommandFleetSend), validator) + case order.CommandTypeScienceCreate: + return decodeCommand(c, new(order.CommandScienceCreate), validator) + case order.CommandTypeScienceRemove: + return decodeCommand(c, new(order.CommandScienceRemove), validator) + case order.CommandTypePlanetRename: + return decodeCommand(c, new(order.CommandPlanetRename), validator) + case order.CommandTypePlanetProduce: + return decodeCommand(c, new(order.CommandPlanetProduce), validator) + case order.CommandTypePlanetRouteSet: + return decodeCommand(c, new(order.CommandPlanetRouteSet), validator) + case order.CommandTypePlanetRouteRemove: + return decodeCommand(c, new(order.CommandPlanetRouteRemove), validator) + default: + return nil, fmt.Errorf("unknown comman type: %s", t) + } +} + +func decodeCommand(m json.RawMessage, c order.DecodableCommand, validator func(order.DecodableCommand) error) (order.DecodableCommand, error) { + v, err := unmarshallCommand(m, c) + if err != nil { + return nil, err + } + if validator != nil { + err = validator(v) + } + if err != nil { + return nil, err + } + return v, nil +} + +func unmarshallCommand[T order.DecodableCommand](c json.RawMessage, v T) (T, error) { + if err := json.Unmarshal(c, v); err != nil { + return v, err + } + return v, nil +} diff --git a/internal/repo/repo.go b/internal/repo/repo.go index 86737ed..4dc2166 100644 --- a/internal/repo/repo.go +++ b/internal/repo/repo.go @@ -28,6 +28,7 @@ type Storage interface { Lock() (func() error, error) Exists(string) (bool, error) Write(string, encoding.BinaryMarshaler) error + WriteSafe(string, encoding.BinaryMarshaler) error Read(string, encoding.BinaryUnmarshaler) error ReadSafe(string, encoding.BinaryUnmarshaler) error } diff --git a/internal/repo/repo_export_test.go b/internal/repo/repo_export_test.go new file mode 100644 index 0000000..09a4b83 --- /dev/null +++ b/internal/repo/repo_export_test.go @@ -0,0 +1,14 @@ +package repo + +import ( + "github.com/google/uuid" + "github.com/iliadenisov/galaxy/internal/model/order" +) + +func LoadOrder_T(s Storage, t uint, id uuid.UUID) (*order.Order, bool, error) { + return loadOrder(s, t, id) +} + +func SaveOrder_T(s Storage, t uint, id uuid.UUID, o *order.Order) error { + return saveOrder(s, t, id, o) +} diff --git a/internal/repo/repo_test.go b/internal/repo/repo_test.go new file mode 100644 index 0000000..d1b3a12 --- /dev/null +++ b/internal/repo/repo_test.go @@ -0,0 +1,119 @@ +package repo_test + +import ( + "path/filepath" + "testing" + + "github.com/google/uuid" + "github.com/iliadenisov/galaxy/internal/model/order" + "github.com/iliadenisov/galaxy/internal/repo" + "github.com/iliadenisov/galaxy/internal/repo/fs" + "github.com/stretchr/testify/assert" +) + +func TestSaveOrder(t *testing.T) { + root := t.ArtifactDir() + s, err := fs.NewFileStorage(root) + assert.NoError(t, err) + id := uuid.New() + o := &order.Order{ + Commands: []order.DecodableCommand{ + &order.CommandRaceVote{ + CommandMeta: order.CommandMeta{ + CmdType: order.CommandTypeRaceVote, + CmdID: uuid.New().String(), + }, + Acceptor: "Race_acc", + }, + &order.CommandShipClassCreate{ + CommandMeta: order.CommandMeta{ + CmdType: order.CommandTypeShipClassCreate, + CmdID: uuid.New().String(), + }, + Name: "Fighter", + Drive: 20.5, + Armament: 5, + Weapons: 20, + Shields: 15.5, + Cargo: 0, + }, + &order.CommandShipGroupMerge{ + CommandMeta: order.CommandMeta{ + CmdType: order.CommandTypeShipGroupMerge, + CmdID: uuid.New().String(), + }, + }, + &order.CommandShipClassCreate{ + CommandMeta: order.CommandMeta{ + CmdType: order.CommandTypeShipClassCreate, + CmdID: uuid.New().String(), + }, + Name: "Freighter", + Drive: 30.33, + Armament: 1, + Weapons: 1, + Shields: 10.1, + Cargo: 0, + }, + &order.CommandRaceQuit{ + CommandMeta: order.CommandMeta{ + CmdType: order.CommandTypeRaceQuit, + CmdID: uuid.New().String(), + }, + }, + }, + } + var turn uint = 2 + + for i := range o.Commands { + if v, ok := order.AsCommand[*order.CommandRaceVote](o.Commands[i]); ok { + m := &v.CommandMeta + m.Result(0) + } else if v, ok := order.AsCommand[*order.CommandRaceQuit](o.Commands[i]); ok { + v.Result(10) + } else if v, ok := order.AsCommand[*order.CommandShipClassCreate](o.Commands[i]); ok { + m := &v.CommandMeta + m.Result(33) + } else if v, ok := order.AsCommand[*order.CommandShipGroupMerge](o.Commands[i]); ok { + v.Result(0) + } + } + + assert.NoError(t, repo.SaveOrder_T(s, turn, id, o)) + assert.FileExists(t, filepath.Join(root, repo.OrderDir(turn, id))) + + LoadOrderTest(t, s, root, turn, id, o) +} + +func LoadOrderTest(t *testing.T, s repo.Storage, root string, turn uint, id uuid.UUID, expected *order.Order) { + o, ok, err := repo.LoadOrder_T(s, turn, id) + assert.NoError(t, err) + assert.True(t, ok) + assert.Len(t, o.Commands, 5) + assert.ElementsMatch(t, expected.Commands, o.Commands) + + CommandResultTest(t, o) +} + +func CommandResultTest(t *testing.T, o *order.Order) { + assert.NotEmpty(t, o.Commands) + for i := range o.Commands { + if v, ok := order.AsCommand[*order.CommandRaceVote](o.Commands[i]); ok { + assert.NotNil(t, v.CmdApplied) + assert.True(t, *v.CmdApplied) + assert.Equal(t, 0, *v.CmdErrCode) + } else if v, ok := order.AsCommand[*order.CommandRaceQuit](o.Commands[i]); ok { + assert.NotNil(t, v.CmdApplied) + assert.False(t, *v.CmdApplied) + assert.Equal(t, 10, *v.CmdErrCode) + } else if v, ok := order.AsCommand[*order.CommandShipClassCreate](o.Commands[i]); ok { + assert.NotNil(t, v.CmdApplied) + assert.False(t, *v.CmdApplied) + assert.Equal(t, 33, *v.CmdErrCode) + } else if v, ok := order.AsCommand[*order.CommandShipGroupMerge](o.Commands[i]); ok { + assert.NotNil(t, v.CmdApplied) + assert.True(t, *v.CmdApplied) + assert.Equal(t, 0, *v.CmdErrCode) + } + } +} diff --git a/internal/router/command_test.go b/internal/router/command_test.go index 28cb590..d2dd288 100644 --- a/internal/router/command_test.go +++ b/internal/router/command_test.go @@ -6,29 +6,19 @@ import ( "net/http/httptest" "testing" - "github.com/google/uuid" + "github.com/iliadenisov/galaxy/internal/model/order" "github.com/iliadenisov/galaxy/internal/model/rest" "github.com/stretchr/testify/assert" ) -var ( - commandNoErrorsStatus = http.StatusNoContent - commandDefaultActor = "Gorlum" - apiCommandMethod = "PUT" - apiCommandPath = "/api/v1/command" - validId1 = uuid.New().String() - validId2 = uuid.New().String() - invalidId = "fd091c69-5976-4775-b2f9-7ba77735afb" -) - func TestCommandRaceQuit(t *testing.T) { r := setupRouter() payload := &rest.Command{ Actor: commandDefaultActor, Commands: []json.RawMessage{ - encodeCommand(&rest.CommandRaceQuit{ - CommandMeta: rest.CommandMeta{Type: rest.CommandTypeRaceQuit}, + encodeCommand(&order.CommandRaceQuit{ + CommandMeta: order.CommandMeta{CmdID: id(), CmdType: order.CommandTypeRaceQuit}, }), }, } @@ -56,8 +46,8 @@ func TestCommandRaceQuit(t *testing.T) { // unrecognized command type payload.Commands = []json.RawMessage{ - encodeCommand(&rest.CommandRaceQuit{ - CommandMeta: rest.CommandMeta{Type: rest.CommandType("-unknown-")}, + encodeCommand(&order.CommandRaceQuit{ + CommandMeta: order.CommandMeta{CmdID: id(), CmdType: order.CommandType("-unknown-")}, }), } w = httptest.NewRecorder() @@ -95,8 +85,8 @@ func TestCommandRaceVote(t *testing.T) { payload := &rest.Command{ Actor: commandDefaultActor, Commands: []json.RawMessage{ - encodeCommand(&rest.CommandRaceVote{ - CommandMeta: rest.CommandMeta{Type: rest.CommandTypeRaceVote}, + encodeCommand(&order.CommandRaceVote{ + CommandMeta: order.CommandMeta{CmdID: id(), CmdType: order.CommandTypeRaceVote}, Acceptor: tc.acceptor, }), }, @@ -133,8 +123,8 @@ func TestCommandRaceRelation(t *testing.T) { payload := &rest.Command{ Actor: commandDefaultActor, Commands: []json.RawMessage{ - encodeCommand(&rest.CommandRaceRelation{ - CommandMeta: rest.CommandMeta{Type: rest.CommandTypeRaceRelation}, + encodeCommand(&order.CommandRaceRelation{ + CommandMeta: order.CommandMeta{CmdID: id(), CmdType: order.CommandTypeRaceRelation}, Acceptor: tc.acceptor, Relation: tc.relation, }), @@ -184,8 +174,8 @@ func TestCommandShipClassCreate(t *testing.T) { payload := &rest.Command{ Actor: commandDefaultActor, Commands: []json.RawMessage{ - encodeCommand(&rest.CommandShipClassCreate{ - CommandMeta: rest.CommandMeta{Type: rest.CommandTypeShipClassCreate}, + encodeCommand(&order.CommandShipClassCreate{ + CommandMeta: order.CommandMeta{CmdID: id(), CmdType: order.CommandTypeShipClassCreate}, Name: tc.name, Drive: tc.D, Armament: tc.A, @@ -227,8 +217,8 @@ func TestCommandShipClassMerge(t *testing.T) { payload := &rest.Command{ Actor: commandDefaultActor, Commands: []json.RawMessage{ - encodeCommand(&rest.CommandShipClassMerge{ - CommandMeta: rest.CommandMeta{Type: rest.CommandTypeShipClassMerge}, + encodeCommand(&order.CommandShipClassMerge{ + CommandMeta: order.CommandMeta{CmdID: id(), CmdType: order.CommandTypeShipClassMerge}, Name: tc.name, Target: tc.target, }), @@ -261,8 +251,8 @@ func TestCommandShipClassRemove(t *testing.T) { payload := &rest.Command{ Actor: commandDefaultActor, Commands: []json.RawMessage{ - encodeCommand(&rest.CommandShipClassRemove{ - CommandMeta: rest.CommandMeta{Type: rest.CommandTypeShipClassRemove}, + encodeCommand(&order.CommandShipClassRemove{ + CommandMeta: order.CommandMeta{CmdID: id(), CmdType: order.CommandTypeShipClassRemove}, Name: tc.name, }), }, @@ -300,8 +290,8 @@ func TestCommandShipGroupBreak(t *testing.T) { payload := &rest.Command{ Actor: commandDefaultActor, Commands: []json.RawMessage{ - encodeCommand(&rest.CommandShipGroupBreak{ - CommandMeta: rest.CommandMeta{Type: rest.CommandTypeShipGroupBreak}, + encodeCommand(&order.CommandShipGroupBreak{ + CommandMeta: order.CommandMeta{CmdID: id(), CmdType: order.CommandTypeShipGroupBreak}, ID: tc.id, NewID: tc.newId, Quantity: tc.quantity, @@ -341,8 +331,8 @@ func TestCommandShipGroupLoad(t *testing.T) { payload := &rest.Command{ Actor: commandDefaultActor, Commands: []json.RawMessage{ - encodeCommand(&rest.CommandShipGroupLoad{ - CommandMeta: rest.CommandMeta{Type: rest.CommandTypeShipGroupLoad}, + encodeCommand(&order.CommandShipGroupLoad{ + CommandMeta: order.CommandMeta{CmdID: id(), CmdType: order.CommandTypeShipGroupLoad}, ID: tc.id, Cargo: tc.cargo, Quantity: tc.quantity, @@ -378,8 +368,8 @@ func TestCommandShipGroupUnload(t *testing.T) { payload := &rest.Command{ Actor: commandDefaultActor, Commands: []json.RawMessage{ - encodeCommand(&rest.CommandShipGroupUnload{ - CommandMeta: rest.CommandMeta{Type: rest.CommandTypeShipGroupUnload}, + encodeCommand(&order.CommandShipGroupUnload{ + CommandMeta: order.CommandMeta{CmdID: id(), CmdType: order.CommandTypeShipGroupUnload}, ID: tc.id, Quantity: tc.quantity, }), @@ -414,8 +404,8 @@ func TestCommandShipGroupSend(t *testing.T) { payload := &rest.Command{ Actor: commandDefaultActor, Commands: []json.RawMessage{ - encodeCommand(&rest.CommandShipGroupSend{ - CommandMeta: rest.CommandMeta{Type: rest.CommandTypeShipGroupSend}, + encodeCommand(&order.CommandShipGroupSend{ + CommandMeta: order.CommandMeta{CmdID: id(), CmdType: order.CommandTypeShipGroupSend}, ID: tc.id, Destination: tc.destination, }), @@ -456,8 +446,8 @@ func TestCommandShipGroupUpgrade(t *testing.T) { payload := &rest.Command{ Actor: commandDefaultActor, Commands: []json.RawMessage{ - encodeCommand(&rest.CommandShipGroupUpgrade{ - CommandMeta: rest.CommandMeta{Type: rest.CommandTypeShipGroupUpgrade}, + encodeCommand(&order.CommandShipGroupUpgrade{ + CommandMeta: order.CommandMeta{CmdID: id(), CmdType: order.CommandTypeShipGroupUpgrade}, ID: tc.id, Tech: tc.tech, Level: tc.level, @@ -487,8 +477,8 @@ func TestCommandShipGroupMerge(t *testing.T) { payload := &rest.Command{ Actor: commandDefaultActor, Commands: []json.RawMessage{ - encodeCommand(&rest.CommandShipGroupMerge{ - CommandMeta: rest.CommandMeta{Type: rest.CommandTypeShipGroupMerge}, + encodeCommand(&order.CommandShipGroupMerge{ + CommandMeta: order.CommandMeta{CmdID: id(), CmdType: order.CommandTypeShipGroupMerge}, }), }, } @@ -518,8 +508,8 @@ func TestCommandShipGroupDismantle(t *testing.T) { payload := &rest.Command{ Actor: commandDefaultActor, Commands: []json.RawMessage{ - encodeCommand(&rest.CommandShipGroupDismantle{ - CommandMeta: rest.CommandMeta{Type: rest.CommandTypeShipGroupDismantle}, + encodeCommand(&order.CommandShipGroupDismantle{ + CommandMeta: order.CommandMeta{CmdID: id(), CmdType: order.CommandTypeShipGroupDismantle}, ID: tc.id, }), }, @@ -554,8 +544,8 @@ func TestCommandShipGroupTransfer(t *testing.T) { payload := &rest.Command{ Actor: commandDefaultActor, Commands: []json.RawMessage{ - encodeCommand(&rest.CommandShipGroupTransfer{ - CommandMeta: rest.CommandMeta{Type: rest.CommandTypeShipGroupTransfer}, + encodeCommand(&order.CommandShipGroupTransfer{ + CommandMeta: order.CommandMeta{CmdID: id(), CmdType: order.CommandTypeShipGroupTransfer}, ID: tc.id, Acceptor: tc.acceptor, }), @@ -591,8 +581,8 @@ func TestCommandShipGroupJoinFleet(t *testing.T) { payload := &rest.Command{ Actor: commandDefaultActor, Commands: []json.RawMessage{ - encodeCommand(&rest.CommandShipGroupJoinFleet{ - CommandMeta: rest.CommandMeta{Type: rest.CommandTypeShipGroupJoinFleet}, + encodeCommand(&order.CommandShipGroupJoinFleet{ + CommandMeta: order.CommandMeta{CmdID: id(), CmdType: order.CommandTypeShipGroupJoinFleet}, ID: tc.id, Name: tc.name, }), @@ -628,8 +618,8 @@ func TestCommandFleetMerge(t *testing.T) { payload := &rest.Command{ Actor: commandDefaultActor, Commands: []json.RawMessage{ - encodeCommand(&rest.CommandFleetMerge{ - CommandMeta: rest.CommandMeta{Type: rest.CommandTypeFleetMerge}, + encodeCommand(&order.CommandFleetMerge{ + CommandMeta: order.CommandMeta{CmdID: id(), CmdType: order.CommandTypeFleetMerge}, Name: tc.name, Target: tc.target, }), @@ -664,8 +654,8 @@ func TestCommandFleetSend(t *testing.T) { payload := &rest.Command{ Actor: commandDefaultActor, Commands: []json.RawMessage{ - encodeCommand(&rest.CommandFleetSend{ - CommandMeta: rest.CommandMeta{Type: rest.CommandTypeFleetSend}, + encodeCommand(&order.CommandFleetSend{ + CommandMeta: order.CommandMeta{CmdID: id(), CmdType: order.CommandTypeFleetSend}, Name: tc.name, Destination: tc.destination, }), @@ -706,8 +696,8 @@ func TestCommandScienceCreate(t *testing.T) { payload := &rest.Command{ Actor: commandDefaultActor, Commands: []json.RawMessage{ - encodeCommand(&rest.CommandScienceCreate{ - CommandMeta: rest.CommandMeta{Type: rest.CommandTypeScienceCreate}, + encodeCommand(&order.CommandScienceCreate{ + CommandMeta: order.CommandMeta{CmdID: id(), CmdType: order.CommandTypeScienceCreate}, Name: tc.name, Drive: tc.D, Weapons: tc.W, @@ -743,8 +733,8 @@ func TestCommandScienceRemove(t *testing.T) { payload := &rest.Command{ Actor: commandDefaultActor, Commands: []json.RawMessage{ - encodeCommand(&rest.CommandScienceRemove{ - CommandMeta: rest.CommandMeta{Type: rest.CommandTypeScienceRemove}, + encodeCommand(&order.CommandScienceRemove{ + CommandMeta: order.CommandMeta{CmdID: id(), CmdType: order.CommandTypeScienceRemove}, Name: tc.name, }), }, @@ -779,8 +769,8 @@ func TestCommandPlanetRename(t *testing.T) { payload := &rest.Command{ Actor: commandDefaultActor, Commands: []json.RawMessage{ - encodeCommand(&rest.CommandPlanetRename{ - CommandMeta: rest.CommandMeta{Type: rest.CommandTypePlanetRename}, + encodeCommand(&order.CommandPlanetRename{ + CommandMeta: order.CommandMeta{CmdID: id(), CmdType: order.CommandTypePlanetRename}, Number: tc.number, Name: tc.name, }), @@ -825,8 +815,8 @@ func TestCommandPlanetProduce(t *testing.T) { payload := &rest.Command{ Actor: commandDefaultActor, Commands: []json.RawMessage{ - encodeCommand(&rest.CommandPlanetProduce{ - CommandMeta: rest.CommandMeta{Type: rest.CommandTypePlanetProduce}, + encodeCommand(&order.CommandPlanetProduce{ + CommandMeta: order.CommandMeta{CmdID: id(), CmdType: order.CommandTypePlanetProduce}, Number: tc.number, Production: tc.production, Subject: tc.subject, @@ -866,8 +856,8 @@ func TestCommandPlanetRouteSet(t *testing.T) { payload := &rest.Command{ Actor: commandDefaultActor, Commands: []json.RawMessage{ - encodeCommand(&rest.CommandPlanetRouteSet{ - CommandMeta: rest.CommandMeta{Type: rest.CommandTypePlanetRouteSet}, + encodeCommand(&order.CommandPlanetRouteSet{ + CommandMeta: order.CommandMeta{CmdID: id(), CmdType: order.CommandTypePlanetRouteSet}, Origin: tc.origin, Destination: tc.destination, LoadType: tc.loadType, @@ -905,8 +895,8 @@ func TestCommandPlanetRouteRemove(t *testing.T) { payload := &rest.Command{ Actor: commandDefaultActor, Commands: []json.RawMessage{ - encodeCommand(&rest.CommandPlanetRouteRemove{ - CommandMeta: rest.CommandMeta{Type: rest.CommandTypePlanetRouteRemove}, + encodeCommand(&order.CommandPlanetRouteRemove{ + CommandMeta: order.CommandMeta{CmdID: id(), CmdType: order.CommandTypePlanetRouteRemove}, Origin: tc.origin, LoadType: tc.loadType, }), @@ -929,13 +919,13 @@ func TestMultipleCommands(t *testing.T) { payload := &rest.Command{ Actor: commandDefaultActor, Commands: []json.RawMessage{ - encodeCommand(&rest.CommandRaceRelation{ - CommandMeta: rest.CommandMeta{Type: rest.CommandTypeRaceRelation}, + encodeCommand(&order.CommandRaceRelation{ + CommandMeta: order.CommandMeta{CmdID: id(), CmdType: order.CommandTypeRaceRelation}, Acceptor: "Opponent", Relation: "PEACE", }), - encodeCommand(&rest.CommandRaceVote{ - CommandMeta: rest.CommandMeta{Type: rest.CommandTypeRaceVote}, + encodeCommand(&order.CommandRaceVote{ + CommandMeta: order.CommandMeta{CmdID: id(), CmdType: order.CommandTypeRaceVote}, Acceptor: "Opponent", }), }, diff --git a/internal/router/handler/command.go b/internal/router/handler/command.go index f117006..5beb5e1 100644 --- a/internal/router/handler/command.go +++ b/internal/router/handler/command.go @@ -2,6 +2,7 @@ package handler import ( "encoding/json" + "errors" "fmt" "net/http" @@ -11,29 +12,30 @@ import ( "github.com/gin-gonic/gin" "github.com/gin-gonic/gin/binding" + "github.com/iliadenisov/galaxy/internal/model/order" "github.com/iliadenisov/galaxy/internal/model/rest" ) func CommandHandler(c *gin.Context, executor CommandExecutor) { var cmd rest.Command - if errorResponded(c, c.ShouldBindJSON(&cmd)) { + if errorResponse(c, c.ShouldBindJSON(&cmd)) { return } - commands := make([]Command, 0) + commands := make([]Command, len(cmd.Commands)) for i := range cmd.Commands { command, err := parseCommand(cmd.Actor, cmd.Commands[i]) - if errorResponded(c, err) { + if errorResponse(c, err) { return } - commands = append(commands, command) + commands[i] = command } if len(commands) == 0 { - c.JSON(http.StatusBadRequest, gin.H{"error": "no commands given"}) + errorResponse(c, errors.New("no commands given")) return } - if errorResponded(c, executor.Execute(commands...)) { + if errorResponse(c, executor.Execute(commands...)) { return } @@ -41,56 +43,56 @@ func CommandHandler(c *gin.Context, executor CommandExecutor) { } func parseCommand(actor string, c json.RawMessage) (Command, error) { - meta := new(rest.CommandMeta) + meta := new(order.CommandMeta) if err := json.Unmarshal(c, meta); err != nil { return nil, err } - switch t := meta.Type; t { - case rest.CommandTypeRaceQuit: + switch t := meta.CmdType; t { + case order.CommandTypeRaceQuit: return commandRaceQuit(actor) - case rest.CommandTypeRaceVote: + case order.CommandTypeRaceVote: return commandRaceVote(actor, c) - case rest.CommandTypeRaceRelation: + case order.CommandTypeRaceRelation: return commandRaceRelation(actor, c) - case rest.CommandTypeShipClassCreate: + case order.CommandTypeShipClassCreate: return commandShipClassCreate(actor, c) - case rest.CommandTypeShipClassMerge: + case order.CommandTypeShipClassMerge: return commandShipClassMerge(actor, c) - case rest.CommandTypeShipClassRemove: + case order.CommandTypeShipClassRemove: return commandShipClassRemove(actor, c) - case rest.CommandTypeShipGroupBreak: + case order.CommandTypeShipGroupBreak: return commandShipGroupBreak(actor, c) - case rest.CommandTypeShipGroupLoad: + case order.CommandTypeShipGroupLoad: return commandShipGroupLoad(actor, c) - case rest.CommandTypeShipGroupUnload: + case order.CommandTypeShipGroupUnload: return commandShipGroupUnload(actor, c) - case rest.CommandTypeShipGroupSend: + case order.CommandTypeShipGroupSend: return commandShipGroupSend(actor, c) - case rest.CommandTypeShipGroupUpgrade: + case order.CommandTypeShipGroupUpgrade: return commandShipGroupUpgrade(actor, c) - case rest.CommandTypeShipGroupMerge: + case order.CommandTypeShipGroupMerge: return commandShipGroupMerge(actor, c) - case rest.CommandTypeShipGroupDismantle: + case order.CommandTypeShipGroupDismantle: return commandShipGroupDismantle(actor, c) - case rest.CommandTypeShipGroupTransfer: + case order.CommandTypeShipGroupTransfer: return commandShipGroupTransfer(actor, c) - case rest.CommandTypeShipGroupJoinFleet: + case order.CommandTypeShipGroupJoinFleet: return commandShipGroupJoinFleet(actor, c) - case rest.CommandTypeFleetMerge: + case order.CommandTypeFleetMerge: return commandFleetMerge(actor, c) - case rest.CommandTypeFleetSend: + case order.CommandTypeFleetSend: return commandFleetSend(actor, c) - case rest.CommandTypeScienceCreate: + case order.CommandTypeScienceCreate: return commandScienceCreate(actor, c) - case rest.CommandTypeScienceRemove: + case order.CommandTypeScienceRemove: return commandScienceRemove(actor, c) - case rest.CommandTypePlanetRename: + case order.CommandTypePlanetRename: return commandPlanetRename(actor, c) - case rest.CommandTypePlanetProduce: + case order.CommandTypePlanetProduce: return commandPlanetProduce(actor, c) - case rest.CommandTypePlanetRouteSet: + case order.CommandTypePlanetRouteSet: return commandPlanetRouteSet(actor, c) - case rest.CommandTypePlanetRouteRemove: + case order.CommandTypePlanetRouteRemove: return commandPlanetRouteRemove(actor, c) default: return nil, fmt.Errorf("unknown comman type: %s", t) @@ -102,7 +104,7 @@ func commandRaceQuit(actor string) (Command, error) { } func commandRaceVote(actor string, c json.RawMessage) (Command, error) { - if v, err := unmarshallCommand(c, new(rest.CommandRaceVote)); err != nil { + if v, err := unmarshallCommand(c, new(order.CommandRaceVote)); err != nil { return nil, err } else { return func(c controller.Ctrl) error { @@ -112,7 +114,7 @@ func commandRaceVote(actor string, c json.RawMessage) (Command, error) { } func commandRaceRelation(actor string, c json.RawMessage) (Command, error) { - if v, err := unmarshallCommand(c, new(rest.CommandRaceRelation)); err != nil { + if v, err := unmarshallCommand(c, new(order.CommandRaceRelation)); err != nil { return nil, err } else { return func(c controller.Ctrl) error { @@ -122,7 +124,7 @@ func commandRaceRelation(actor string, c json.RawMessage) (Command, error) { } func commandShipClassCreate(actor string, c json.RawMessage) (Command, error) { - if v, err := unmarshallCommand(c, new(rest.CommandShipClassCreate)); err != nil { + if v, err := unmarshallCommand(c, new(order.CommandShipClassCreate)); err != nil { return nil, err } else { return func(c controller.Ctrl) error { @@ -132,7 +134,7 @@ func commandShipClassCreate(actor string, c json.RawMessage) (Command, error) { } func commandShipClassMerge(actor string, c json.RawMessage) (Command, error) { - if v, err := unmarshallCommand(c, new(rest.CommandShipClassMerge)); err != nil { + if v, err := unmarshallCommand(c, new(order.CommandShipClassMerge)); err != nil { return nil, err } else { return func(c controller.Ctrl) error { @@ -142,7 +144,7 @@ func commandShipClassMerge(actor string, c json.RawMessage) (Command, error) { } func commandShipClassRemove(actor string, c json.RawMessage) (Command, error) { - if v, err := unmarshallCommand(c, new(rest.CommandShipClassRemove)); err != nil { + if v, err := unmarshallCommand(c, new(order.CommandShipClassRemove)); err != nil { return nil, err } else { return func(c controller.Ctrl) error { @@ -152,7 +154,7 @@ func commandShipClassRemove(actor string, c json.RawMessage) (Command, error) { } func commandShipGroupBreak(actor string, c json.RawMessage) (Command, error) { - if v, err := unmarshallCommand(c, new(rest.CommandShipGroupBreak)); err != nil { + if v, err := unmarshallCommand(c, new(order.CommandShipGroupBreak)); err != nil { return nil, err } else { return func(c controller.Ctrl) error { @@ -162,7 +164,7 @@ func commandShipGroupBreak(actor string, c json.RawMessage) (Command, error) { } func commandShipGroupLoad(actor string, c json.RawMessage) (Command, error) { - if v, err := unmarshallCommand(c, new(rest.CommandShipGroupLoad)); err != nil { + if v, err := unmarshallCommand(c, new(order.CommandShipGroupLoad)); err != nil { return nil, err } else { return func(c controller.Ctrl) error { @@ -172,7 +174,7 @@ func commandShipGroupLoad(actor string, c json.RawMessage) (Command, error) { } func commandShipGroupUnload(actor string, c json.RawMessage) (Command, error) { - if v, err := unmarshallCommand(c, new(rest.CommandShipGroupUnload)); err != nil { + if v, err := unmarshallCommand(c, new(order.CommandShipGroupUnload)); err != nil { return nil, err } else { return func(c controller.Ctrl) error { @@ -182,7 +184,7 @@ func commandShipGroupUnload(actor string, c json.RawMessage) (Command, error) { } func commandShipGroupSend(actor string, c json.RawMessage) (Command, error) { - if v, err := unmarshallCommand(c, new(rest.CommandShipGroupSend)); err != nil { + if v, err := unmarshallCommand(c, new(order.CommandShipGroupSend)); err != nil { return nil, err } else { return func(c controller.Ctrl) error { @@ -192,7 +194,7 @@ func commandShipGroupSend(actor string, c json.RawMessage) (Command, error) { } func commandShipGroupUpgrade(actor string, c json.RawMessage) (Command, error) { - if v, err := unmarshallCommand(c, new(rest.CommandShipGroupUpgrade)); err != nil { + if v, err := unmarshallCommand(c, new(order.CommandShipGroupUpgrade)); err != nil { return nil, err } else { return func(c controller.Ctrl) error { @@ -208,7 +210,7 @@ func commandShipGroupMerge(actor string, c json.RawMessage) (Command, error) { } func commandShipGroupDismantle(actor string, c json.RawMessage) (Command, error) { - if v, err := unmarshallCommand(c, new(rest.CommandShipGroupDismantle)); err != nil { + if v, err := unmarshallCommand(c, new(order.CommandShipGroupDismantle)); err != nil { return nil, err } else { return func(c controller.Ctrl) error { @@ -218,7 +220,7 @@ func commandShipGroupDismantle(actor string, c json.RawMessage) (Command, error) } func commandShipGroupTransfer(actor string, c json.RawMessage) (Command, error) { - if v, err := unmarshallCommand(c, new(rest.CommandShipGroupTransfer)); err != nil { + if v, err := unmarshallCommand(c, new(order.CommandShipGroupTransfer)); err != nil { return nil, err } else { return func(c controller.Ctrl) error { @@ -228,7 +230,7 @@ func commandShipGroupTransfer(actor string, c json.RawMessage) (Command, error) } func commandShipGroupJoinFleet(actor string, c json.RawMessage) (Command, error) { - if v, err := unmarshallCommand(c, new(rest.CommandShipGroupJoinFleet)); err != nil { + if v, err := unmarshallCommand(c, new(order.CommandShipGroupJoinFleet)); err != nil { return nil, err } else { return func(c controller.Ctrl) error { @@ -238,7 +240,7 @@ func commandShipGroupJoinFleet(actor string, c json.RawMessage) (Command, error) } func commandFleetMerge(actor string, c json.RawMessage) (Command, error) { - if v, err := unmarshallCommand(c, new(rest.CommandFleetMerge)); err != nil { + if v, err := unmarshallCommand(c, new(order.CommandFleetMerge)); err != nil { return nil, err } else { return func(c controller.Ctrl) error { @@ -248,7 +250,7 @@ func commandFleetMerge(actor string, c json.RawMessage) (Command, error) { } func commandFleetSend(actor string, c json.RawMessage) (Command, error) { - if v, err := unmarshallCommand(c, new(rest.CommandFleetSend)); err != nil { + if v, err := unmarshallCommand(c, new(order.CommandFleetSend)); err != nil { return nil, err } else { return func(c controller.Ctrl) error { @@ -258,7 +260,7 @@ func commandFleetSend(actor string, c json.RawMessage) (Command, error) { } func commandScienceCreate(actor string, c json.RawMessage) (Command, error) { - if v, err := unmarshallCommand(c, new(rest.CommandScienceCreate)); err != nil { + if v, err := unmarshallCommand(c, new(order.CommandScienceCreate)); err != nil { return nil, err } else { return func(c controller.Ctrl) error { @@ -268,7 +270,7 @@ func commandScienceCreate(actor string, c json.RawMessage) (Command, error) { } func commandScienceRemove(actor string, c json.RawMessage) (Command, error) { - if v, err := unmarshallCommand(c, new(rest.CommandScienceRemove)); err != nil { + if v, err := unmarshallCommand(c, new(order.CommandScienceRemove)); err != nil { return nil, err } else { return func(c controller.Ctrl) error { @@ -278,7 +280,7 @@ func commandScienceRemove(actor string, c json.RawMessage) (Command, error) { } func commandPlanetRename(actor string, c json.RawMessage) (Command, error) { - if v, err := unmarshallCommand(c, new(rest.CommandPlanetRename)); err != nil { + if v, err := unmarshallCommand(c, new(order.CommandPlanetRename)); err != nil { return nil, err } else { return func(c controller.Ctrl) error { @@ -288,7 +290,7 @@ func commandPlanetRename(actor string, c json.RawMessage) (Command, error) { } func commandPlanetProduce(actor string, c json.RawMessage) (Command, error) { - if v, err := unmarshallCommand(c, new(rest.CommandPlanetProduce)); err != nil { + if v, err := unmarshallCommand(c, new(order.CommandPlanetProduce)); err != nil { return nil, err } else { return func(c controller.Ctrl) error { @@ -298,7 +300,7 @@ func commandPlanetProduce(actor string, c json.RawMessage) (Command, error) { } func commandPlanetRouteSet(actor string, c json.RawMessage) (Command, error) { - if v, err := unmarshallCommand(c, new(rest.CommandPlanetRouteSet)); err != nil { + if v, err := unmarshallCommand(c, new(order.CommandPlanetRouteSet)); err != nil { return nil, err } else { return func(c controller.Ctrl) error { @@ -308,7 +310,7 @@ func commandPlanetRouteSet(actor string, c json.RawMessage) (Command, error) { } func commandPlanetRouteRemove(actor string, c json.RawMessage) (Command, error) { - if v, err := unmarshallCommand(c, new(rest.CommandPlanetRouteRemove)); err != nil { + if v, err := unmarshallCommand(c, new(order.CommandPlanetRouteRemove)); err != nil { return nil, err } else { return func(c controller.Ctrl) error { @@ -319,17 +321,17 @@ func commandPlanetRouteRemove(actor string, c json.RawMessage) (Command, error) // Helpers -func unmarshallCommand[T rest.DecodableCommand](c json.RawMessage, v *T) (*T, error) { +func unmarshallCommand[T order.DecodableCommand](c json.RawMessage, v T) (T, error) { if err := json.Unmarshal(c, v); err != nil { - return nil, err + return v, err } if err := validateCommand(v); err != nil { - return nil, err + return v, err } return v, nil } -func validateCommand(v any) error { +func validateCommand(v order.DecodableCommand) error { if ve, ok := binding.Validator.Engine().(*validator.Validate); ok { if err := ve.Struct(v); err != nil { return err diff --git a/internal/router/handler/handler.go b/internal/router/handler/handler.go index e9edd8d..5cfc6d4 100644 --- a/internal/router/handler/handler.go +++ b/internal/router/handler/handler.go @@ -10,6 +10,7 @@ import ( "github.com/iliadenisov/galaxy/internal/controller" e "github.com/iliadenisov/galaxy/internal/error" "github.com/iliadenisov/galaxy/internal/model/game" + "github.com/iliadenisov/galaxy/internal/model/order" "github.com/iliadenisov/galaxy/internal/model/rest" ) @@ -18,6 +19,7 @@ type CommandExecutor interface { GenerateTurn() (rest.StateResponse, error) GameState() (rest.StateResponse, error) Execute(cmd ...Command) error + ValidateOrder(actor string, cmd ...order.DecodableCommand) error } type Command func(controller.Ctrl) error @@ -40,10 +42,10 @@ func NewDefaultConfigExecutor(configurer controller.Configurer) CommandExecutor return &executor{cfg: configurer} } -func (e *executor) Execute(command ...Command) error { +func (e *executor) Execute(cmd ...Command) error { return controller.ExecuteCommand(e.cfg, func(c controller.Ctrl) error { - for i := range command { - if err := command[i](c); err != nil { + for i := range cmd { + if err := cmd[i](c); err != nil { return err } } @@ -51,6 +53,10 @@ func (e *executor) Execute(command ...Command) error { }) } +func (e *executor) ValidateOrder(actor string, cmd ...order.DecodableCommand) error { + return controller.ValidateOrder(e.cfg, actor, cmd...) +} + func (e *executor) GenerateGame(races []string) (rest.StateResponse, error) { s, err := controller.GenerateGame(e.cfg, races) if err != nil { @@ -90,19 +96,17 @@ func stateResponse(s game.State) rest.StateResponse { return *result } -func errorResponded(c *gin.Context, err error) bool { +func errorResponse(c *gin.Context, err error) bool { if err == nil { return false } - var ge = new(e.GenericError) - if v, ok := err.(validator.ValidationErrors); ok { c.JSON(http.StatusBadRequest, gin.H{"error": v.Error()}) return true } - if errors.As(err, ge) { + if ge, ok := errors.AsType[*e.GenericError](err); ok { switch ge.Code { case e.ErrGameNotInitialized: c.Status(http.StatusNotImplemented) diff --git a/internal/router/handler/init.go b/internal/router/handler/init.go index 4e8d41c..df59222 100644 --- a/internal/router/handler/init.go +++ b/internal/router/handler/init.go @@ -9,7 +9,7 @@ import ( func InitHandler(c *gin.Context, executor CommandExecutor) { var init rest.Init - if errorResponded(c, c.ShouldBindJSON(&init)) { + if errorResponse(c, c.ShouldBindJSON(&init)) { return } @@ -19,7 +19,7 @@ func InitHandler(c *gin.Context, executor CommandExecutor) { } s, err := executor.GenerateGame(races) - if errorResponded(c, err) { + if errorResponse(c, err) { return } diff --git a/internal/router/handler/order.go b/internal/router/handler/order.go new file mode 100644 index 0000000..d9e0b1c --- /dev/null +++ b/internal/router/handler/order.go @@ -0,0 +1,37 @@ +package handler + +import ( + "errors" + "net/http" + + "github.com/gin-gonic/gin" + "github.com/iliadenisov/galaxy/internal/model/order" + "github.com/iliadenisov/galaxy/internal/model/rest" + "github.com/iliadenisov/galaxy/internal/repo" +) + +func OrderHandler(c *gin.Context, executor CommandExecutor) { + var cmd rest.Command + if errorResponse(c, c.ShouldBindJSON(&cmd)) { + return + } + + commands := make([]order.DecodableCommand, len(cmd.Commands)) + for i := range cmd.Commands { + command, err := repo.ParseOrder(cmd.Commands[i], validateCommand) + if errorResponse(c, err) { + return + } + commands[i] = command + } + if len(commands) == 0 { + errorResponse(c, errors.New("no commands given")) + return + } + + if errorResponse(c, executor.ValidateOrder(cmd.Actor, commands...)) { + return + } + + c.Status(http.StatusNoContent) +} diff --git a/internal/router/handler/status.go b/internal/router/handler/status.go index 7e05a93..5762e58 100644 --- a/internal/router/handler/status.go +++ b/internal/router/handler/status.go @@ -9,7 +9,7 @@ import ( func StatusHandler(c *gin.Context, executor CommandExecutor) { state, err := executor.GameState() - if errorResponded(c, err) { + if errorResponse(c, err) { return } diff --git a/internal/router/handler/turn.go b/internal/router/handler/turn.go index 8af1834..4b9aff7 100644 --- a/internal/router/handler/turn.go +++ b/internal/router/handler/turn.go @@ -9,7 +9,7 @@ import ( func TurnHandler(c *gin.Context, executor CommandExecutor) { state, err := executor.GenerateTurn() - if errorResponded(c, err) { + if errorResponse(c, err) { return } diff --git a/internal/router/order_test.go b/internal/router/order_test.go new file mode 100644 index 0000000..bf1b0a6 --- /dev/null +++ b/internal/router/order_test.go @@ -0,0 +1,941 @@ +package router_test + +import ( + "encoding/json" + "net/http" + "net/http/httptest" + "testing" + + "github.com/iliadenisov/galaxy/internal/model/order" + "github.com/iliadenisov/galaxy/internal/model/rest" + "github.com/stretchr/testify/assert" +) + +func TestOrderRaceQuit(t *testing.T) { + r := setupRouter() + + payload := &rest.Command{ + Actor: commandDefaultActor, + Commands: []json.RawMessage{ + encodeCommand(&order.CommandRaceQuit{ + CommandMeta: order.CommandMeta{CmdID: id(), CmdType: order.CommandTypeRaceQuit}, + }), + }, + } + + w := httptest.NewRecorder() + req, _ := http.NewRequest(apiCommandMethod, apiOrderPath, asBody(payload)) + r.ServeHTTP(w, req) + + assert.Equal(t, commandNoErrorsStatus, w.Code, w.Body) + + // error: actor not set + payload.Actor = "" + w = httptest.NewRecorder() + req, _ = http.NewRequest(apiCommandMethod, apiOrderPath, asBody(payload)) + r.ServeHTTP(w, req) + + assert.Equal(t, http.StatusBadRequest, w.Code, w.Body) + + payload.Actor = " " + w = httptest.NewRecorder() + req, _ = http.NewRequest(apiCommandMethod, apiOrderPath, asBody(payload)) + r.ServeHTTP(w, req) + + assert.Equal(t, http.StatusBadRequest, w.Code, w.Body) + + // unrecognized command type + payload.Commands = []json.RawMessage{ + encodeCommand(&order.CommandRaceQuit{ + CommandMeta: order.CommandMeta{CmdID: id(), CmdType: order.CommandType("-unknown-")}, + }), + } + w = httptest.NewRecorder() + req, _ = http.NewRequest(apiCommandMethod, apiOrderPath, asBody(payload)) + r.ServeHTTP(w, req) + + assert.Equal(t, http.StatusBadRequest, w.Code, w.Body) + + // error: no commands + payload = &rest.Command{ + Actor: commandDefaultActor, + } + + w = httptest.NewRecorder() + req, _ = http.NewRequest(apiCommandMethod, apiOrderPath, asBody(payload)) + r.ServeHTTP(w, req) + + assert.Equal(t, http.StatusBadRequest, w.Code, w.Body) +} + +func TestOrderRaceVote(t *testing.T) { + r := setupRouter() + + for _, tc := range []struct { + expectStatus int + description string + acceptor string + }{ + {commandNoErrorsStatus, "Valid request", "AnotherRace"}, + {http.StatusBadRequest, "Empty acceptor", ""}, + {http.StatusBadRequest, "Blank acceptor", " "}, + {http.StatusBadRequest, "Invalid acceptor", "Race_👽"}, + } { + t.Run(tc.description, func(t *testing.T) { + payload := &rest.Command{ + Actor: commandDefaultActor, + Commands: []json.RawMessage{ + encodeCommand(&order.CommandRaceVote{ + CommandMeta: order.CommandMeta{CmdID: id(), CmdType: order.CommandTypeRaceVote}, + Acceptor: tc.acceptor, + }), + }, + } + + w := httptest.NewRecorder() + req, _ := http.NewRequest(apiCommandMethod, apiOrderPath, asBody(payload)) + r.ServeHTTP(w, req) + + assert.Equal(t, tc.expectStatus, w.Code, w.Body) + }) + } +} + +func TestOrderRaceRelation(t *testing.T) { + r := setupRouter() + + for _, tc := range []struct { + expectStatus int + description string + relation string + acceptor string + }{ + {commandNoErrorsStatus, "Valid request 1", "WAR", "Opponent"}, + {commandNoErrorsStatus, "Valid request 2", "PEACE", "Opponent"}, + {http.StatusBadRequest, "Empty relation", "", "Opponent"}, + {http.StatusBadRequest, "Blank relation", " ", "Opponent"}, + {http.StatusBadRequest, "Invalid relation", "Woina", "Opponent"}, + {http.StatusBadRequest, "Empty acceptor", "WAR", ""}, + {http.StatusBadRequest, "Blank acceptor", "WAR", " "}, + {http.StatusBadRequest, "Invalid acceptor", "PEACE", "Race_👽"}, + } { + t.Run(tc.description, func(t *testing.T) { + payload := &rest.Command{ + Actor: commandDefaultActor, + Commands: []json.RawMessage{ + encodeCommand(&order.CommandRaceRelation{ + CommandMeta: order.CommandMeta{CmdID: id(), CmdType: order.CommandTypeRaceRelation}, + Acceptor: tc.acceptor, + Relation: tc.relation, + }), + }, + } + + w := httptest.NewRecorder() + req, _ := http.NewRequest(apiCommandMethod, apiOrderPath, asBody(payload)) + r.ServeHTTP(w, req) + + assert.Equal(t, tc.expectStatus, w.Code, w.Body) + }) + } +} + +func TestOrderShipClassCreate(t *testing.T) { + r := setupRouter() + + for _, tc := range []struct { + D float64 + A int + W, S, C float64 + name string + expectStatus int + description string + }{ + {1, 0, 0, 0, 0, "Drone", commandNoErrorsStatus, "Simple Drone"}, + {1, 1, 1, 0, 0, "Drone", commandNoErrorsStatus, "Armed Drone"}, + {1, 0, 0, 1, 0, "Drone", commandNoErrorsStatus, "Shielded Drone"}, + {1, 0, 0, 0, 1, "Drone", commandNoErrorsStatus, "Carrying Drone"}, + {1, 0, 0, 0, 0, "", http.StatusBadRequest, "Empty name"}, + {1, 0, 0, 0, 0, " ", http.StatusBadRequest, "Blank name"}, + {1, 0, 0, 0, 0, "Drone🚀", http.StatusBadRequest, "Invalid name"}, + {-0.5, 0, 0, 0, 0, "Drone", http.StatusBadRequest, "Drive less than 0"}, + {0.9, 0, 0, 0, 0, "Drone", http.StatusBadRequest, "Drive less than 1"}, + {1, 1, 0, 0, 0, "Drone", http.StatusBadRequest, "Ammo without Weapons"}, + {1, 0, 1, 0, 0, "Drone", http.StatusBadRequest, "Weapons without Ammo"}, + {1, -1, 1, 0, 0, "Drone", http.StatusBadRequest, "Ammo less than 0"}, + {1, 1, 0.9, 0, 0, "Drone", http.StatusBadRequest, "Weapons less than 1"}, + {1, 1, -0.5, 0, 0, "Drone", http.StatusBadRequest, "Weapons less than 0"}, + {1, 0, 0, -0.5, 0, "Drone", http.StatusBadRequest, "Shields less than 0"}, + {1, 0, 0, 0.9, 0, "Drone", http.StatusBadRequest, "Shields less than 1"}, + {1, 0, 0, 0, -0.5, "Drone", http.StatusBadRequest, "Cargo less than 0"}, + {1, 0, 0, 0, 0.9, "Drone", http.StatusBadRequest, "Cargo less than 1"}, + } { + t.Run(tc.description, func(t *testing.T) { + payload := &rest.Command{ + Actor: commandDefaultActor, + Commands: []json.RawMessage{ + encodeCommand(&order.CommandShipClassCreate{ + CommandMeta: order.CommandMeta{CmdID: id(), CmdType: order.CommandTypeShipClassCreate}, + Name: tc.name, + Drive: tc.D, + Armament: tc.A, + Weapons: tc.W, + Shields: tc.S, + Cargo: tc.C, + }), + }, + } + + w := httptest.NewRecorder() + req, _ := http.NewRequest(apiCommandMethod, apiOrderPath, asBody(payload)) + r.ServeHTTP(w, req) + + assert.Equal(t, tc.expectStatus, w.Code, w.Body) + }) + } +} + +func TestOrderShipClassMerge(t *testing.T) { + r := setupRouter() + + for _, tc := range []struct { + expectStatus int + description string + name string + target string + }{ + {commandNoErrorsStatus, "Valid request", "Drone", "Spy"}, + {http.StatusBadRequest, "Empty name", "", "Spy"}, + {http.StatusBadRequest, "Blank name", " ", "Spy"}, + {http.StatusBadRequest, "Invalid name", "Drone🚀", "Spy"}, + {http.StatusBadRequest, "Empty name", "Drone", " "}, + {http.StatusBadRequest, "Blank name", "Drone", " "}, + {http.StatusBadRequest, "Invalid name", "Drone", "Spy🚀"}, + {http.StatusBadRequest, "Equal names", "Drone", "Drone"}, + } { + t.Run(tc.description, func(t *testing.T) { + payload := &rest.Command{ + Actor: commandDefaultActor, + Commands: []json.RawMessage{ + encodeCommand(&order.CommandShipClassMerge{ + CommandMeta: order.CommandMeta{CmdID: id(), CmdType: order.CommandTypeShipClassMerge}, + Name: tc.name, + Target: tc.target, + }), + }, + } + + w := httptest.NewRecorder() + req, _ := http.NewRequest(apiCommandMethod, apiOrderPath, asBody(payload)) + r.ServeHTTP(w, req) + + assert.Equal(t, tc.expectStatus, w.Code, w.Body) + }) + } +} + +func TestOrderShipClassRemove(t *testing.T) { + r := setupRouter() + + for _, tc := range []struct { + expectStatus int + description string + name string + }{ + {commandNoErrorsStatus, "Valid request", "Drone"}, + {http.StatusBadRequest, "Empty name", ""}, + {http.StatusBadRequest, "Blank name", " "}, + {http.StatusBadRequest, "Invalid name", "Drone🚀"}, + } { + t.Run(tc.description, func(t *testing.T) { + payload := &rest.Command{ + Actor: commandDefaultActor, + Commands: []json.RawMessage{ + encodeCommand(&order.CommandShipClassRemove{ + CommandMeta: order.CommandMeta{CmdID: id(), CmdType: order.CommandTypeShipClassRemove}, + Name: tc.name, + }), + }, + } + + w := httptest.NewRecorder() + req, _ := http.NewRequest(apiCommandMethod, apiOrderPath, asBody(payload)) + r.ServeHTTP(w, req) + + assert.Equal(t, tc.expectStatus, w.Code, w.Body) + }) + } +} + +func TestOrderShipGroupBreak(t *testing.T) { + r := setupRouter() + + for _, tc := range []struct { + expectStatus int + description string + id string + newId string + quantity int + }{ + {commandNoErrorsStatus, "Valid request #1", validId1, validId2, 1}, + {commandNoErrorsStatus, "Valid request #2", validId1, validId2, 0}, + {http.StatusBadRequest, "Negative quantity", validId1, validId2, -1}, + {http.StatusBadRequest, "Empty id", "", validId2, 1}, + {http.StatusBadRequest, "Invalid id", invalidId, validId2, 1}, + {http.StatusBadRequest, "Empty newId", validId1, "", 1}, + {http.StatusBadRequest, "Invalid newId", validId1, invalidId, 1}, + {http.StatusBadRequest, "Equal id and newId", validId1, validId1, 1}, + } { + t.Run(tc.description, func(t *testing.T) { + payload := &rest.Command{ + Actor: commandDefaultActor, + Commands: []json.RawMessage{ + encodeCommand(&order.CommandShipGroupBreak{ + CommandMeta: order.CommandMeta{CmdID: id(), CmdType: order.CommandTypeShipGroupBreak}, + ID: tc.id, + NewID: tc.newId, + Quantity: tc.quantity, + }), + }, + } + + w := httptest.NewRecorder() + req, _ := http.NewRequest(apiCommandMethod, apiOrderPath, asBody(payload)) + r.ServeHTTP(w, req) + + assert.Equal(t, tc.expectStatus, w.Code, w.Body) + }) + } +} + +func TestOrderShipGroupLoad(t *testing.T) { + r := setupRouter() + + for _, tc := range []struct { + expectStatus int + description string + id string + cargo string + quantity float64 + }{ + {commandNoErrorsStatus, "Valid request #1", validId1, "COL", 0}, + {commandNoErrorsStatus, "Valid request #2", validId1, "MAT", 1}, + {commandNoErrorsStatus, "Valid request #2", validId1, "CAP", 2}, + {http.StatusBadRequest, "Invalid quantity", validId1, "COL", -0.5}, + {http.StatusBadRequest, "Empty cargo", validId1, "", 1}, + {http.StatusBadRequest, "Invalid cargo", validId1, "IND", 1}, + {http.StatusBadRequest, "Empty id", "", "COL", 1}, + {http.StatusBadRequest, "Invalid id", invalidId, "COL", 1}, + } { + t.Run(tc.description, func(t *testing.T) { + payload := &rest.Command{ + Actor: commandDefaultActor, + Commands: []json.RawMessage{ + encodeCommand(&order.CommandShipGroupLoad{ + CommandMeta: order.CommandMeta{CmdID: id(), CmdType: order.CommandTypeShipGroupLoad}, + ID: tc.id, + Cargo: tc.cargo, + Quantity: tc.quantity, + }), + }, + } + + w := httptest.NewRecorder() + req, _ := http.NewRequest(apiCommandMethod, apiOrderPath, asBody(payload)) + r.ServeHTTP(w, req) + + assert.Equal(t, tc.expectStatus, w.Code, w.Body) + }) + } +} + +func TestOrderShipGroupUnload(t *testing.T) { + r := setupRouter() + + for _, tc := range []struct { + expectStatus int + description string + id string + quantity float64 + }{ + {commandNoErrorsStatus, "Valid request #1", validId1, 0}, + {commandNoErrorsStatus, "Valid request #2", validId1, 1}, + {http.StatusBadRequest, "Invalid quantity", validId1, -0.5}, + {http.StatusBadRequest, "Empty id", "", 1}, + {http.StatusBadRequest, "Invalid id", invalidId, 1}, + } { + t.Run(tc.description, func(t *testing.T) { + payload := &rest.Command{ + Actor: commandDefaultActor, + Commands: []json.RawMessage{ + encodeCommand(&order.CommandShipGroupUnload{ + CommandMeta: order.CommandMeta{CmdID: id(), CmdType: order.CommandTypeShipGroupUnload}, + ID: tc.id, + Quantity: tc.quantity, + }), + }, + } + + w := httptest.NewRecorder() + req, _ := http.NewRequest(apiCommandMethod, apiOrderPath, asBody(payload)) + r.ServeHTTP(w, req) + + assert.Equal(t, tc.expectStatus, w.Code, w.Body) + }) + } +} + +func TestOrderShipGroupSend(t *testing.T) { + r := setupRouter() + + for _, tc := range []struct { + expectStatus int + description string + id string + destination int + }{ + {commandNoErrorsStatus, "Valid request #1", validId1, 0}, + {commandNoErrorsStatus, "Valid request #1", validId1, 1}, + {http.StatusBadRequest, "Invalid destination", validId1, -1}, + {http.StatusBadRequest, "Empty id", "", 1}, + {http.StatusBadRequest, "Invalid id", invalidId, 1}, + } { + t.Run(tc.description, func(t *testing.T) { + payload := &rest.Command{ + Actor: commandDefaultActor, + Commands: []json.RawMessage{ + encodeCommand(&order.CommandShipGroupSend{ + CommandMeta: order.CommandMeta{CmdID: id(), CmdType: order.CommandTypeShipGroupSend}, + ID: tc.id, + Destination: tc.destination, + }), + }, + } + + w := httptest.NewRecorder() + req, _ := http.NewRequest(apiCommandMethod, apiOrderPath, asBody(payload)) + r.ServeHTTP(w, req) + + assert.Equal(t, tc.expectStatus, w.Code, w.Body) + }) + } +} + +func TestOrderShipGroupUpgrade(t *testing.T) { + r := setupRouter() + + for _, tc := range []struct { + expectStatus int + description string + id string + tech string + level float64 + }{ + {commandNoErrorsStatus, "Valid request #1", validId1, "ALL", 0}, + {commandNoErrorsStatus, "Valid request #1", validId1, "DRIVE", 1.1}, + {commandNoErrorsStatus, "Valid request #1", validId1, "WEAPONS", 2.1}, + {commandNoErrorsStatus, "Valid request #1", validId1, "SHIELDS", 3.1}, + {commandNoErrorsStatus, "Valid request #1", validId1, "CARGO", 4.1}, + {http.StatusBadRequest, "Negative level", validId1, "DRIVE", -0.5}, + {http.StatusBadRequest, "Invalid level 0.5", validId1, "DRIVE", 0.5}, + {http.StatusBadRequest, "Invalid level 1.0", validId1, "DRIVE", 1.0}, + {http.StatusBadRequest, "Empty id", "", "ALL", 0}, + {http.StatusBadRequest, "Invalid id", invalidId, "ALL", 0}, + } { + t.Run(tc.description, func(t *testing.T) { + payload := &rest.Command{ + Actor: commandDefaultActor, + Commands: []json.RawMessage{ + encodeCommand(&order.CommandShipGroupUpgrade{ + CommandMeta: order.CommandMeta{CmdID: id(), CmdType: order.CommandTypeShipGroupUpgrade}, + ID: tc.id, + Tech: tc.tech, + Level: tc.level, + }), + }, + } + + w := httptest.NewRecorder() + req, _ := http.NewRequest(apiCommandMethod, apiOrderPath, asBody(payload)) + r.ServeHTTP(w, req) + + assert.Equal(t, tc.expectStatus, w.Code, w.Body) + }) + } +} + +func TestOrderShipGroupMerge(t *testing.T) { + r := setupRouter() + + for _, tc := range []struct { + expectStatus int + description string + }{ + {commandNoErrorsStatus, "Valid request"}, + } { + t.Run(tc.description, func(t *testing.T) { + payload := &rest.Command{ + Actor: commandDefaultActor, + Commands: []json.RawMessage{ + encodeCommand(&order.CommandShipGroupMerge{ + CommandMeta: order.CommandMeta{CmdID: id(), CmdType: order.CommandTypeShipGroupMerge}, + }), + }, + } + + w := httptest.NewRecorder() + req, _ := http.NewRequest(apiCommandMethod, apiOrderPath, asBody(payload)) + r.ServeHTTP(w, req) + + assert.Equal(t, tc.expectStatus, w.Code, w.Body) + }) + } +} + +func TestOrderShipGroupDismantle(t *testing.T) { + r := setupRouter() + + for _, tc := range []struct { + expectStatus int + description string + id string + }{ + {commandNoErrorsStatus, "Valid request", validId1}, + {http.StatusBadRequest, "Empty id", ""}, + {http.StatusBadRequest, "Invalid id", invalidId}, + } { + t.Run(tc.description, func(t *testing.T) { + payload := &rest.Command{ + Actor: commandDefaultActor, + Commands: []json.RawMessage{ + encodeCommand(&order.CommandShipGroupDismantle{ + CommandMeta: order.CommandMeta{CmdID: id(), CmdType: order.CommandTypeShipGroupDismantle}, + ID: tc.id, + }), + }, + } + + w := httptest.NewRecorder() + req, _ := http.NewRequest(apiCommandMethod, apiOrderPath, asBody(payload)) + r.ServeHTTP(w, req) + + assert.Equal(t, tc.expectStatus, w.Code, w.Body) + }) + } +} + +func TestOrderShipGroupTransfer(t *testing.T) { + r := setupRouter() + + for _, tc := range []struct { + expectStatus int + description string + id string + acceptor string + }{ + {commandNoErrorsStatus, "Valid request", validId1, "AnotherRace"}, + {http.StatusBadRequest, "Blank id", "", "AnotherRace"}, + {http.StatusBadRequest, "Invalid id", invalidId, "AnotherRace"}, + {http.StatusBadRequest, "Empty acceptor", validId1, ""}, + {http.StatusBadRequest, "Blank acceptor", validId1, " "}, + {http.StatusBadRequest, "Invalid acceptor", validId1, "Race_👽"}, + } { + t.Run(tc.description, func(t *testing.T) { + payload := &rest.Command{ + Actor: commandDefaultActor, + Commands: []json.RawMessage{ + encodeCommand(&order.CommandShipGroupTransfer{ + CommandMeta: order.CommandMeta{CmdID: id(), CmdType: order.CommandTypeShipGroupTransfer}, + ID: tc.id, + Acceptor: tc.acceptor, + }), + }, + } + + w := httptest.NewRecorder() + req, _ := http.NewRequest(apiCommandMethod, apiOrderPath, asBody(payload)) + r.ServeHTTP(w, req) + + assert.Equal(t, tc.expectStatus, w.Code, w.Body) + }) + } +} + +func TestOrderShipGroupJoinFleet(t *testing.T) { + r := setupRouter() + + for _, tc := range []struct { + expectStatus int + description string + id string + name string + }{ + {commandNoErrorsStatus, "Valid request", validId1, "AnotherRace"}, + {http.StatusBadRequest, "Blank id", "", "AnotherRace"}, + {http.StatusBadRequest, "Invalid id", invalidId, "AnotherRace"}, + {http.StatusBadRequest, "Empty name", validId1, ""}, + {http.StatusBadRequest, "Blank name", validId1, " "}, + {http.StatusBadRequest, "Invalid name", validId1, "Fleet_🚢"}, + } { + t.Run(tc.description, func(t *testing.T) { + payload := &rest.Command{ + Actor: commandDefaultActor, + Commands: []json.RawMessage{ + encodeCommand(&order.CommandShipGroupJoinFleet{ + CommandMeta: order.CommandMeta{CmdID: id(), CmdType: order.CommandTypeShipGroupJoinFleet}, + ID: tc.id, + Name: tc.name, + }), + }, + } + + w := httptest.NewRecorder() + req, _ := http.NewRequest(apiCommandMethod, apiOrderPath, asBody(payload)) + r.ServeHTTP(w, req) + + assert.Equal(t, tc.expectStatus, w.Code, w.Body) + }) + } +} + +func TestOrderFleetMerge(t *testing.T) { + r := setupRouter() + + for _, tc := range []struct { + expectStatus int + description string + name string + target string + }{ + {commandNoErrorsStatus, "Valid request", "Fleet", "Bomber"}, + {http.StatusBadRequest, "Empty name", "", "Bomber"}, + {http.StatusBadRequest, "Invalid name", "Fleet_🚢", "Bomber"}, + {http.StatusBadRequest, "Empty target", "Fleet", ""}, + {http.StatusBadRequest, "Invalid target", "Fleet", "Bomber_🚢"}, + {http.StatusBadRequest, "Equal name and target", "Fleet", "Fleet"}, + } { + t.Run(tc.description, func(t *testing.T) { + payload := &rest.Command{ + Actor: commandDefaultActor, + Commands: []json.RawMessage{ + encodeCommand(&order.CommandFleetMerge{ + CommandMeta: order.CommandMeta{CmdID: id(), CmdType: order.CommandTypeFleetMerge}, + Name: tc.name, + Target: tc.target, + }), + }, + } + + w := httptest.NewRecorder() + req, _ := http.NewRequest(apiCommandMethod, apiOrderPath, asBody(payload)) + r.ServeHTTP(w, req) + + assert.Equal(t, tc.expectStatus, w.Code, w.Body) + }) + } +} + +func TestOrderFleetSend(t *testing.T) { + r := setupRouter() + + for _, tc := range []struct { + expectStatus int + description string + name string + destination int + }{ + {commandNoErrorsStatus, "Valid request #1", "Fleet", 0}, + {commandNoErrorsStatus, "Valid request #2", "Fleet", 1}, + {http.StatusBadRequest, "Invalid destination", "Fleet", -1}, + {http.StatusBadRequest, "Empty name", "", 1}, + {http.StatusBadRequest, "Invalid name", "Fleet_🚢", 1}, + } { + t.Run(tc.description, func(t *testing.T) { + payload := &rest.Command{ + Actor: commandDefaultActor, + Commands: []json.RawMessage{ + encodeCommand(&order.CommandFleetSend{ + CommandMeta: order.CommandMeta{CmdID: id(), CmdType: order.CommandTypeFleetSend}, + Name: tc.name, + Destination: tc.destination, + }), + }, + } + + w := httptest.NewRecorder() + req, _ := http.NewRequest(apiCommandMethod, apiOrderPath, asBody(payload)) + r.ServeHTTP(w, req) + + assert.Equal(t, tc.expectStatus, w.Code, w.Body) + }) + } +} + +func TestOrderScienceCreate(t *testing.T) { + r := setupRouter() + + for _, tc := range []struct { + expectStatus int + description string + D, W, S, C float64 + name string + }{ + {commandNoErrorsStatus, "Valid request", 0.25, 0.25, 0.25, 0.25, "Science"}, + {http.StatusBadRequest, "Empty name", 0.25, 0.25, 0.25, 0.25, ""}, + {http.StatusBadRequest, "Invalid name", 0.25, 0.25, 0.25, 0.25, "Science🧪"}, + {http.StatusBadRequest, "Negative drive", -.5, 0.25, 0.25, 0.25, "Science"}, + {http.StatusBadRequest, "Negative weapons", 0.25, -.5, 0.25, 0.25, "Science"}, + {http.StatusBadRequest, "Negative shields", 0.25, 0.25, -.5, 0.25, "Science"}, + {http.StatusBadRequest, "Negative cargo", 0.25, 0.25, 0.25, -.5, "Science"}, + {http.StatusBadRequest, "Too big drive", 1.1, 0.25, 0.25, 0.25, "Science"}, + {http.StatusBadRequest, "Too big weapons", 0.25, 1.05, 0.25, 0.25, "Science"}, + {http.StatusBadRequest, "Too big shields", 0.25, 0.25, 1.5, 0.25, "Science"}, + {http.StatusBadRequest, "Too big cargo", 0.25, 0.25, 0.25, 1.01, "Science"}, + } { + t.Run(tc.description, func(t *testing.T) { + payload := &rest.Command{ + Actor: commandDefaultActor, + Commands: []json.RawMessage{ + encodeCommand(&order.CommandScienceCreate{ + CommandMeta: order.CommandMeta{CmdID: id(), CmdType: order.CommandTypeScienceCreate}, + Name: tc.name, + Drive: tc.D, + Weapons: tc.W, + Shields: tc.S, + Cargo: tc.C, + }), + }, + } + + w := httptest.NewRecorder() + req, _ := http.NewRequest(apiCommandMethod, apiOrderPath, asBody(payload)) + r.ServeHTTP(w, req) + + assert.Equal(t, tc.expectStatus, w.Code, w.Body) + }) + } +} + +func TestOrderScienceRemove(t *testing.T) { + r := setupRouter() + + for _, tc := range []struct { + expectStatus int + description string + name string + }{ + {commandNoErrorsStatus, "Valid request", "Drone"}, + {http.StatusBadRequest, "Empty name", ""}, + {http.StatusBadRequest, "Blank name", " "}, + {http.StatusBadRequest, "Invalid name", "Science🧪"}, + } { + t.Run(tc.description, func(t *testing.T) { + payload := &rest.Command{ + Actor: commandDefaultActor, + Commands: []json.RawMessage{ + encodeCommand(&order.CommandScienceRemove{ + CommandMeta: order.CommandMeta{CmdID: id(), CmdType: order.CommandTypeScienceRemove}, + Name: tc.name, + }), + }, + } + + w := httptest.NewRecorder() + req, _ := http.NewRequest(apiCommandMethod, apiOrderPath, asBody(payload)) + r.ServeHTTP(w, req) + + assert.Equal(t, tc.expectStatus, w.Code, w.Body) + }) + } +} + +func TestOrderPlanetRename(t *testing.T) { + r := setupRouter() + + for _, tc := range []struct { + expectStatus int + description string + number int + name string + }{ + {commandNoErrorsStatus, "Valid request #1", 0, "HW"}, + {commandNoErrorsStatus, "Valid request #2", 1, "HW"}, + {http.StatusBadRequest, "Invalid number", -1, "HW"}, + {http.StatusBadRequest, "Empty name", 1, ""}, + {http.StatusBadRequest, "Blank name", 1, " "}, + {http.StatusBadRequest, "Invalid name", 1, "Planet🪐"}, + } { + t.Run(tc.description, func(t *testing.T) { + payload := &rest.Command{ + Actor: commandDefaultActor, + Commands: []json.RawMessage{ + encodeCommand(&order.CommandPlanetRename{ + CommandMeta: order.CommandMeta{CmdID: id(), CmdType: order.CommandTypePlanetRename}, + Number: tc.number, + Name: tc.name, + }), + }, + } + + w := httptest.NewRecorder() + req, _ := http.NewRequest(apiCommandMethod, apiOrderPath, asBody(payload)) + r.ServeHTTP(w, req) + + assert.Equal(t, tc.expectStatus, w.Code, w.Body) + }) + } +} + +func TestOrderPlanetProduce(t *testing.T) { + r := setupRouter() + + for _, tc := range []struct { + expectStatus int + description string + number int + production, subject string + }{ + {commandNoErrorsStatus, "Valid request MAT", 0, "MAT", ""}, + {commandNoErrorsStatus, "Valid request CAP", 1, "CAP", ""}, + {commandNoErrorsStatus, "Valid request DRIVE", 2, "DRIVE", ""}, + {commandNoErrorsStatus, "Valid request WEAPONS", 3, "WEAPONS", ""}, + {commandNoErrorsStatus, "Valid request SHIELDS", 4, "SHIELDS", ""}, + {commandNoErrorsStatus, "Valid request CARGO", 5, "CARGO", ""}, + {commandNoErrorsStatus, "Valid request SCIENCE", 6, "SCIENCE", "Science"}, + {commandNoErrorsStatus, "Valid request SHIP", 7, "SHIP", "Ship"}, + {http.StatusBadRequest, "Empty production", 0, "", ""}, + {http.StatusBadRequest, "Invalid production", 0, "IND", ""}, + {http.StatusBadRequest, "Invalid planet", -1, "DRIVE", ""}, + {http.StatusBadRequest, "Empty science subject", 6, "SCIENCE", ""}, + {http.StatusBadRequest, "Invalid science subject", 6, "SCIENCE", "Science🧪"}, + {http.StatusBadRequest, "Empty ship subject", 6, "SHIP", ""}, + {http.StatusBadRequest, "Invalid ship subject", 6, "SHIP", "Ship🚀"}, + } { + t.Run(tc.description, func(t *testing.T) { + payload := &rest.Command{ + Actor: commandDefaultActor, + Commands: []json.RawMessage{ + encodeCommand(&order.CommandPlanetProduce{ + CommandMeta: order.CommandMeta{CmdID: id(), CmdType: order.CommandTypePlanetProduce}, + Number: tc.number, + Production: tc.production, + Subject: tc.subject, + }), + }, + } + + w := httptest.NewRecorder() + req, _ := http.NewRequest(apiCommandMethod, apiOrderPath, asBody(payload)) + r.ServeHTTP(w, req) + + assert.Equal(t, tc.expectStatus, w.Code, w.Body) + }) + } +} + +func TestOrderPlanetRouteSet(t *testing.T) { + r := setupRouter() + + for _, tc := range []struct { + expectStatus int + description string + origin, destination int + loadType string + }{ + {commandNoErrorsStatus, "Valid request MAT", 1, 0, "MAT"}, + {commandNoErrorsStatus, "Valid request CAP", 0, 1, "CAP"}, + {commandNoErrorsStatus, "Valid request COL", 1, 2, "COL"}, + {commandNoErrorsStatus, "Valid request EMP", 3, 0, "EMP"}, + {http.StatusBadRequest, "Empty loadType", 0, 1, ""}, + {http.StatusBadRequest, "Invalid loadType", 0, 1, "IND"}, + {http.StatusBadRequest, "Invalid origin", -1, 1, "MAT"}, + {http.StatusBadRequest, "Invalid destination", 1, -1, "MAT"}, + {http.StatusBadRequest, "Origin equals destination", 1, 1, "COL"}, + } { + t.Run(tc.description, func(t *testing.T) { + payload := &rest.Command{ + Actor: commandDefaultActor, + Commands: []json.RawMessage{ + encodeCommand(&order.CommandPlanetRouteSet{ + CommandMeta: order.CommandMeta{CmdID: id(), CmdType: order.CommandTypePlanetRouteSet}, + Origin: tc.origin, + Destination: tc.destination, + LoadType: tc.loadType, + }), + }, + } + + w := httptest.NewRecorder() + req, _ := http.NewRequest(apiCommandMethod, apiOrderPath, asBody(payload)) + r.ServeHTTP(w, req) + + assert.Equal(t, tc.expectStatus, w.Code, w.Body) + }) + } +} + +func TestOrderPlanetRouteRemove(t *testing.T) { + r := setupRouter() + + for _, tc := range []struct { + expectStatus int + description string + origin int + loadType string + }{ + {commandNoErrorsStatus, "Valid request MAT", 0, "MAT"}, + {commandNoErrorsStatus, "Valid request CAP", 1, "CAP"}, + {commandNoErrorsStatus, "Valid request COL", 2, "COL"}, + {commandNoErrorsStatus, "Valid request EMP", 0, "EMP"}, + {http.StatusBadRequest, "Empty loadType", 1, ""}, + {http.StatusBadRequest, "Invalid loadType", 1, "IND"}, + {http.StatusBadRequest, "Invalid origin", -1, "MAT"}, + } { + t.Run(tc.description, func(t *testing.T) { + payload := &rest.Command{ + Actor: commandDefaultActor, + Commands: []json.RawMessage{ + encodeCommand(&order.CommandPlanetRouteRemove{ + CommandMeta: order.CommandMeta{CmdID: id(), CmdType: order.CommandTypePlanetRouteRemove}, + Origin: tc.origin, + LoadType: tc.loadType, + }), + }, + } + + w := httptest.NewRecorder() + req, _ := http.NewRequest(apiCommandMethod, apiOrderPath, asBody(payload)) + r.ServeHTTP(w, req) + + assert.Equal(t, tc.expectStatus, w.Code, w.Body) + }) + } +} + +func TestMultipleCommandOrder(t *testing.T) { + e := newExecutor() + r := setupRouterExecutor(e) + + payload := &rest.Command{ + Actor: commandDefaultActor, + Commands: []json.RawMessage{ + encodeCommand(&order.CommandRaceRelation{ + CommandMeta: order.CommandMeta{CmdID: id(), CmdType: order.CommandTypeRaceRelation}, + Acceptor: "Opponent", + Relation: "PEACE", + }), + encodeCommand(&order.CommandRaceVote{ + CommandMeta: order.CommandMeta{CmdID: id(), CmdType: order.CommandTypeRaceVote}, + Acceptor: "Opponent", + }), + }, + } + + w := httptest.NewRecorder() + req, _ := http.NewRequest(apiCommandMethod, apiOrderPath, asBody(payload)) + r.ServeHTTP(w, req) + + assert.Equal(t, commandNoErrorsStatus, w.Code, w.Body) + + assert.Equal(t, 2, e.(*dummyExecutor).CommandsExecuted) +} diff --git a/internal/router/router.go b/internal/router/router.go index f3fdeb3..2bbe036 100644 --- a/internal/router/router.go +++ b/internal/router/router.go @@ -67,6 +67,7 @@ func setupRouter(executor handler.CommandExecutor) *gin.Engine { groupV1.GET("/status", func(ctx *gin.Context) { handler.StatusHandler(ctx, executor) }) groupV1.POST("/init", func(ctx *gin.Context) { handler.InitHandler(ctx, executor) }) groupV1.PUT("/command", LimitMiddleware(1), func(ctx *gin.Context) { handler.CommandHandler(ctx, executor) }) + groupV1.PUT("/order", func(ctx *gin.Context) { handler.OrderHandler(ctx, executor) }) groupV1.PUT("/turn", func(ctx *gin.Context) { handler.TurnHandler(ctx, executor) }) return r diff --git a/internal/router/router_helper_test.go b/internal/router/router_helper_test.go index 4cb8d18..1e19d75 100644 --- a/internal/router/router_helper_test.go +++ b/internal/router/router_helper_test.go @@ -2,17 +2,40 @@ package router_test import ( "encoding/json" + "net/http" "github.com/gin-gonic/gin" + "github.com/google/uuid" + "github.com/iliadenisov/galaxy/internal/model/order" "github.com/iliadenisov/galaxy/internal/model/rest" "github.com/iliadenisov/galaxy/internal/router" "github.com/iliadenisov/galaxy/internal/router/handler" ) +var ( + commandNoErrorsStatus = http.StatusNoContent + commandDefaultActor = "Gorlum" + apiCommandMethod = "PUT" + apiCommandPath = "/api/v1/command" + apiOrderPath = "/api/v1/order" + validId1 = id() + validId2 = id() + invalidId = "fd091c69-5976-4775-b2f9-7ba77735afb" +) + +func id() string { + return uuid.New().String() +} + type dummyExecutor struct { CommandsExecuted int } +func (e *dummyExecutor) ValidateOrder(actor string, cmd ...order.DecodableCommand) error { + e.CommandsExecuted = len(cmd) + return nil +} + func (e *dummyExecutor) Execute(command ...handler.Command) error { e.CommandsExecuted = len(command) return nil