feat: command validation
This commit is contained in:
@@ -10,8 +10,8 @@ type Command struct {
|
|||||||
type CommandType string
|
type CommandType string
|
||||||
|
|
||||||
const (
|
const (
|
||||||
CommandTypeRaceQuit CommandType = "quit"
|
CommandTypeRaceQuit CommandType = "raceQuit"
|
||||||
CommandTypeRaceVote CommandType = "vote"
|
CommandTypeRaceVote CommandType = "raceVote"
|
||||||
CommandTypeRaceRelation CommandType = "declarePeace"
|
CommandTypeRaceRelation CommandType = "declarePeace"
|
||||||
CommandTypeShipClassCreate CommandType = "shipClassCreate"
|
CommandTypeShipClassCreate CommandType = "shipClassCreate"
|
||||||
CommandTypeShipClassMerge CommandType = "shipClassMerge"
|
CommandTypeShipClassMerge CommandType = "shipClassMerge"
|
||||||
@@ -36,16 +36,30 @@ const (
|
|||||||
)
|
)
|
||||||
|
|
||||||
type CommandMeta struct {
|
type CommandMeta struct {
|
||||||
Type CommandType `json:"@type"`
|
Type CommandType `json:"@type" binding:"required,notblank"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type CommandVote struct {
|
type CommandRaceQuit struct {
|
||||||
|
CommandMeta
|
||||||
|
}
|
||||||
|
|
||||||
|
type CommandRaceVote struct {
|
||||||
CommandMeta
|
CommandMeta
|
||||||
Recipient string `json:"recipient" binding:"required,notblank"`
|
Recipient string `json:"recipient" binding:"required,notblank"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type CommandUpdateRelation struct {
|
type CommandRaceRelation struct {
|
||||||
CommandMeta
|
CommandMeta
|
||||||
Opponent string `json:"recipient" binding:"required,notblank"`
|
Opponent string `json:"recipient" binding:"required,notblank"`
|
||||||
Relation string `json:"relation" binding:"required,notblank"`
|
Relation string `json:"relation" binding:"required,notblank"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type CommandShipClassCreate struct {
|
||||||
|
CommandMeta
|
||||||
|
Name string `json:"name" binding:"required,notblank"`
|
||||||
|
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"`
|
||||||
|
}
|
||||||
|
|||||||
@@ -10,13 +10,17 @@ import (
|
|||||||
"github.com/stretchr/testify/assert"
|
"github.com/stretchr/testify/assert"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
commandNoErrorCode = http.StatusNoContent
|
||||||
|
)
|
||||||
|
|
||||||
func TestCommand(t *testing.T) {
|
func TestCommand(t *testing.T) {
|
||||||
r := setupRouter()
|
r := setupRouter()
|
||||||
|
|
||||||
payload := rest.Command{
|
payload := &rest.Command{
|
||||||
Actor: "SomeRace",
|
Actor: "SomeRace",
|
||||||
Commands: []json.RawMessage{
|
Commands: []json.RawMessage{
|
||||||
encodeCommand(&rest.CommandVote{
|
encodeCommand(&rest.CommandRaceVote{
|
||||||
CommandMeta: rest.CommandMeta{Type: rest.CommandTypeRaceVote},
|
CommandMeta: rest.CommandMeta{Type: rest.CommandTypeRaceVote},
|
||||||
Recipient: "AnotherRace",
|
Recipient: "AnotherRace",
|
||||||
}),
|
}),
|
||||||
@@ -27,7 +31,7 @@ func TestCommand(t *testing.T) {
|
|||||||
req, _ := http.NewRequest("PUT", "/api/v1/command", asBody(payload))
|
req, _ := http.NewRequest("PUT", "/api/v1/command", asBody(payload))
|
||||||
r.ServeHTTP(w, req)
|
r.ServeHTTP(w, req)
|
||||||
|
|
||||||
assert.Equal(t, http.StatusNoContent, w.Code, w.Body)
|
assert.Equal(t, commandNoErrorCode, w.Code, w.Body)
|
||||||
|
|
||||||
// error: notblank validator
|
// error: notblank validator
|
||||||
payload.Actor = ""
|
payload.Actor = ""
|
||||||
@@ -45,7 +49,7 @@ func TestCommand(t *testing.T) {
|
|||||||
assert.Equal(t, http.StatusBadRequest, w.Code, w.Body)
|
assert.Equal(t, http.StatusBadRequest, w.Code, w.Body)
|
||||||
|
|
||||||
// error: no commands
|
// error: no commands
|
||||||
payload = rest.Command{
|
payload = &rest.Command{
|
||||||
Actor: "SomeRace",
|
Actor: "SomeRace",
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -56,10 +60,53 @@ func TestCommand(t *testing.T) {
|
|||||||
assert.Equal(t, http.StatusBadRequest, w.Code, w.Body)
|
assert.Equal(t, http.StatusBadRequest, w.Code, w.Body)
|
||||||
}
|
}
|
||||||
|
|
||||||
func encodeCommand(cmd any) json.RawMessage {
|
func TestCommandShipClassCreate(t *testing.T) {
|
||||||
v, err := json.Marshal(cmd)
|
r := setupRouter()
|
||||||
if err != nil {
|
|
||||||
panic(err)
|
for _, tc := range []struct {
|
||||||
|
D float64
|
||||||
|
A int
|
||||||
|
W, S, C float64
|
||||||
|
expectStatus int
|
||||||
|
description string
|
||||||
|
}{
|
||||||
|
{1, 0, 0, 0, 0, commandNoErrorCode, "Simple Drone"},
|
||||||
|
{1, 1, 1, 0, 0, commandNoErrorCode, "Armed Drone"},
|
||||||
|
{1, 0, 0, 1, 0, commandNoErrorCode, "Shielded Drone"},
|
||||||
|
{1, 0, 0, 0, 1, commandNoErrorCode, "Carrying Drone"},
|
||||||
|
{-0.5, 0, 0, 0, 0, http.StatusBadRequest, "Drive less than 0"},
|
||||||
|
{0.9, 0, 0, 0, 0, http.StatusBadRequest, "Drive less than 1"},
|
||||||
|
{1, 1, 0, 0, 0, http.StatusBadRequest, "Ammo without Weapons"},
|
||||||
|
{1, 0, 1, 0, 0, http.StatusBadRequest, "Weapons without Ammo"},
|
||||||
|
{1, -1, 1, 0, 0, http.StatusBadRequest, "Ammo less than 0"},
|
||||||
|
{1, 1, 0.9, 0, 0, http.StatusBadRequest, "Weapons less than 1"},
|
||||||
|
{1, 1, -0.5, 0, 0, http.StatusBadRequest, "Weapons less than 0"},
|
||||||
|
{1, 0, 0, -0.5, 0, http.StatusBadRequest, "Shields less than 0"},
|
||||||
|
{1, 0, 0, 0.9, 0, http.StatusBadRequest, "Shields less than 1"},
|
||||||
|
{1, 0, 0, 0, -0.5, http.StatusBadRequest, "Cargo less than 0"},
|
||||||
|
{1, 0, 0, 0, 0.9, http.StatusBadRequest, "Cargo less than 1"},
|
||||||
|
} {
|
||||||
|
t.Run(tc.description, func(t *testing.T) {
|
||||||
|
payload := &rest.Command{
|
||||||
|
Actor: "SomeRace",
|
||||||
|
Commands: []json.RawMessage{
|
||||||
|
encodeCommand(&rest.CommandShipClassCreate{
|
||||||
|
CommandMeta: rest.CommandMeta{Type: rest.CommandTypeShipClassCreate},
|
||||||
|
Name: "Ship",
|
||||||
|
Drive: tc.D,
|
||||||
|
Armament: tc.A,
|
||||||
|
Weapons: tc.W,
|
||||||
|
Shields: tc.S,
|
||||||
|
Cargo: tc.C,
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
w := httptest.NewRecorder()
|
||||||
|
req, _ := http.NewRequest("PUT", "/api/v1/command", asBody(payload))
|
||||||
|
r.ServeHTTP(w, req)
|
||||||
|
|
||||||
|
assert.Equal(t, tc.expectStatus, w.Code, w.Body)
|
||||||
|
})
|
||||||
}
|
}
|
||||||
return v
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,9 +5,11 @@ import (
|
|||||||
"fmt"
|
"fmt"
|
||||||
"net/http"
|
"net/http"
|
||||||
|
|
||||||
|
"github.com/go-playground/validator/v10"
|
||||||
"github.com/iliadenisov/galaxy/internal/controller"
|
"github.com/iliadenisov/galaxy/internal/controller"
|
||||||
|
|
||||||
"github.com/gin-gonic/gin"
|
"github.com/gin-gonic/gin"
|
||||||
|
"github.com/gin-gonic/gin/binding"
|
||||||
"github.com/iliadenisov/galaxy/internal/model/rest"
|
"github.com/iliadenisov/galaxy/internal/model/rest"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -43,27 +45,67 @@ func parseCommand(actor string, c json.RawMessage) (Command, error) {
|
|||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
switch t := meta.Type; t {
|
switch t := meta.Type; t {
|
||||||
|
case rest.CommandTypeRaceQuit:
|
||||||
|
return commandRaceQuit(actor)
|
||||||
case rest.CommandTypeRaceVote:
|
case rest.CommandTypeRaceVote:
|
||||||
return giveVotes(actor, c)
|
return commandRaceVote(actor, c)
|
||||||
case rest.CommandTypeRaceRelation:
|
case rest.CommandTypeRaceRelation:
|
||||||
return updateRelation(actor, c)
|
return commandRaceRelation(actor, c)
|
||||||
|
case rest.CommandTypeShipClassCreate:
|
||||||
|
return commandShipClassCreate(actor, c)
|
||||||
default:
|
default:
|
||||||
return nil, fmt.Errorf("unknown comman type: %s", t)
|
return nil, fmt.Errorf("unknown comman type: %s", t)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func giveVotes(actor string, c json.RawMessage) (Command, error) {
|
func commandRaceQuit(actor string) (Command, error) {
|
||||||
var v rest.CommandVote
|
return func(c controller.Ctrl) error { return c.RaceQuit(actor) }, nil
|
||||||
if err := json.Unmarshal(c, &v); err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
return func(c controller.Ctrl) error { return c.RaceVote(actor, v.Recipient) }, nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func updateRelation(actor string, c json.RawMessage) (Command, error) {
|
func commandRaceVote(actor string, c json.RawMessage) (Command, error) {
|
||||||
var v rest.CommandUpdateRelation
|
var v rest.CommandRaceVote
|
||||||
if err := json.Unmarshal(c, &v); err != nil {
|
if err := json.Unmarshal(c, &v); err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
return func(c controller.Ctrl) error { return c.RaceRelation(actor, v.Opponent, v.Relation) }, nil
|
if err := validateCommand(v); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return func(c controller.Ctrl) error {
|
||||||
|
return c.RaceVote(actor, v.Recipient)
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func commandRaceRelation(actor string, c json.RawMessage) (Command, error) {
|
||||||
|
var v rest.CommandRaceRelation
|
||||||
|
if err := json.Unmarshal(c, &v); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if err := validateCommand(v); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return func(c controller.Ctrl) error {
|
||||||
|
return c.RaceRelation(actor, v.Opponent, v.Relation)
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func commandShipClassCreate(actor string, c json.RawMessage) (Command, error) {
|
||||||
|
v := new(rest.CommandShipClassCreate)
|
||||||
|
if err := json.Unmarshal(c, v); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if err := validateCommand(v); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return func(c controller.Ctrl) error {
|
||||||
|
return c.ShipClassCreate(actor, v.Name, v.Drive, int(v.Armament), v.Weapons, v.Shields, v.Cargo)
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func validateCommand(v any) error {
|
||||||
|
if ve, ok := binding.Validator.Engine().(*validator.Validate); ok {
|
||||||
|
if err := ve.Struct(v); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ package router
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"io"
|
||||||
"net/http"
|
"net/http"
|
||||||
"os"
|
"os"
|
||||||
|
|
||||||
@@ -25,6 +26,7 @@ func (r Router) Run() error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func NewRouter() Router {
|
func NewRouter() Router {
|
||||||
|
gin.SetMode(gin.ReleaseMode)
|
||||||
return NewRouterExecutor(handler.NewDefaultExecutor())
|
return NewRouterExecutor(handler.NewDefaultExecutor())
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -33,17 +35,25 @@ func NewRouterExecutor(executor handler.CommandExecutor) Router {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func setupRouter(executor handler.CommandExecutor) *gin.Engine {
|
func setupRouter(executor handler.CommandExecutor) *gin.Engine {
|
||||||
gin.SetMode(gin.ReleaseMode)
|
|
||||||
r := gin.New()
|
r := gin.New()
|
||||||
|
|
||||||
// Logger middleware will write the logs to gin.DefaultWriter even if you set with GIN_MODE=release.
|
// Logger middleware will write the logs to gin.DefaultWriter even if you set with GIN_MODE=release.
|
||||||
r.Use(gin.LoggerWithFormatter(logFormatter))
|
logConfig := &gin.LoggerConfig{Formatter: logFormatter}
|
||||||
|
if gin.Mode() != gin.DebugMode {
|
||||||
|
logConfig.Output = io.Discard
|
||||||
|
}
|
||||||
|
r.Use(gin.LoggerWithConfig(*logConfig))
|
||||||
|
|
||||||
// Recovery middleware recovers from any panics and writes a 500 if there was one.
|
// Recovery middleware recovers from any panics and writes a 500 if there was one.
|
||||||
r.Use(gin.CustomRecovery(recoveryHandler))
|
r.Use(gin.CustomRecovery(recoveryHandler))
|
||||||
|
|
||||||
if v, ok := binding.Validator.Engine().(*validator.Validate); ok {
|
if v, ok := binding.Validator.Engine().(*validator.Validate); ok {
|
||||||
v.RegisterValidation("notblank", notBlankStringValidator)
|
if err := v.RegisterValidation("notblank", notBlankStringValidator); err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
if err := v.RegisterValidation("ammoWeapons", armamentWithWeaponsValidator); err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
groupV1 := r.Group("/api/v1")
|
groupV1 := r.Group("/api/v1")
|
||||||
|
|||||||
@@ -1,6 +1,8 @@
|
|||||||
package router_test
|
package router_test
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
|
||||||
"github.com/gin-gonic/gin"
|
"github.com/gin-gonic/gin"
|
||||||
"github.com/google/uuid"
|
"github.com/google/uuid"
|
||||||
"github.com/iliadenisov/galaxy/internal/model/rest"
|
"github.com/iliadenisov/galaxy/internal/model/rest"
|
||||||
@@ -24,5 +26,14 @@ func (e *dummyExecutor) GameState() (rest.StateResponse, error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func setupRouter() *gin.Engine {
|
func setupRouter() *gin.Engine {
|
||||||
|
gin.SetMode(gin.TestMode)
|
||||||
return router.SetupRouter(&dummyExecutor{})
|
return router.SetupRouter(&dummyExecutor{})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func encodeCommand(cmd any) json.RawMessage {
|
||||||
|
v, err := json.Marshal(cmd)
|
||||||
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
return v
|
||||||
|
}
|
||||||
|
|||||||
@@ -15,3 +15,27 @@ var notBlankStringValidator validator.Func = func(fl validator.FieldLevel) bool
|
|||||||
}
|
}
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var armamentWithWeaponsValidator validator.Func = func(fl validator.FieldLevel) bool {
|
||||||
|
var v, compareTo float64
|
||||||
|
|
||||||
|
f := fl.Parent().FieldByName(fl.Param())
|
||||||
|
|
||||||
|
if f.CanFloat() {
|
||||||
|
compareTo = f.Float()
|
||||||
|
} else if f.CanInt() {
|
||||||
|
compareTo = float64(f.Int())
|
||||||
|
} else {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
if fl.Field().CanFloat() {
|
||||||
|
v = fl.Field().Float()
|
||||||
|
} else if fl.Field().CanInt() {
|
||||||
|
v = float64(fl.Field().Int())
|
||||||
|
} else {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
return (v == 0 && compareTo == 0) || (v >= 1 && compareTo >= 1)
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user