feat: init api
This commit is contained in:
@@ -35,12 +35,14 @@ type Param struct {
|
|||||||
StoragePath string
|
StoragePath string
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewController(configure func(*Param)) (*Controller, error) {
|
type Config func(*Param)
|
||||||
|
|
||||||
|
func NewController(config Config) (*Controller, error) {
|
||||||
c := &Param{
|
c := &Param{
|
||||||
StoragePath: ".",
|
StoragePath: ".",
|
||||||
}
|
}
|
||||||
if configure != nil {
|
if config != nil {
|
||||||
configure(c)
|
config(c)
|
||||||
}
|
}
|
||||||
r, err := repo.NewFileRepo(c.StoragePath)
|
r, err := repo.NewFileRepo(c.StoragePath)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|||||||
@@ -0,0 +1,15 @@
|
|||||||
|
package rest
|
||||||
|
|
||||||
|
import "github.com/google/uuid"
|
||||||
|
|
||||||
|
type Init struct {
|
||||||
|
Races []Race `json:"races" binding:"required,gte=10"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type Race struct {
|
||||||
|
Name string `json:"name" binding:"required,notblank"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type InitResponse struct {
|
||||||
|
UUID uuid.UUID `json:"uuid"`
|
||||||
|
}
|
||||||
@@ -0,0 +1,71 @@
|
|||||||
|
package router_test
|
||||||
|
|
||||||
|
import (
|
||||||
|
"net/http"
|
||||||
|
"net/http/httptest"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/iliadenisov/galaxy/internal/model/rest"
|
||||||
|
"github.com/iliadenisov/galaxy/internal/router"
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestCommand(t *testing.T) {
|
||||||
|
r := router.SetupRouter()
|
||||||
|
|
||||||
|
payload := rest.Command{
|
||||||
|
Race: "SomeRace",
|
||||||
|
Vote: &rest.CommandVote{
|
||||||
|
Recipient: "AnotherRace",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
w := httptest.NewRecorder()
|
||||||
|
req, _ := http.NewRequest("PUT", "/api/v1/command", asBody(payload))
|
||||||
|
r.ServeHTTP(w, req)
|
||||||
|
|
||||||
|
assert.Equal(t, http.StatusOK, w.Code, w.Body)
|
||||||
|
|
||||||
|
// error: notblank validator
|
||||||
|
payload.Race = ""
|
||||||
|
w = httptest.NewRecorder()
|
||||||
|
req, _ = http.NewRequest("PUT", "/api/v1/command", asBody(payload))
|
||||||
|
r.ServeHTTP(w, req)
|
||||||
|
|
||||||
|
assert.Equal(t, http.StatusBadRequest, w.Code, w.Body)
|
||||||
|
|
||||||
|
payload.Race = " "
|
||||||
|
w = httptest.NewRecorder()
|
||||||
|
req, _ = http.NewRequest("PUT", "/api/v1/command", asBody(payload))
|
||||||
|
r.ServeHTTP(w, req)
|
||||||
|
|
||||||
|
assert.Equal(t, http.StatusBadRequest, w.Code, w.Body)
|
||||||
|
|
||||||
|
// error: no commands
|
||||||
|
payload = rest.Command{
|
||||||
|
Race: "SomeRace",
|
||||||
|
}
|
||||||
|
|
||||||
|
w = httptest.NewRecorder()
|
||||||
|
req, _ = http.NewRequest("PUT", "/api/v1/command", asBody(payload))
|
||||||
|
r.ServeHTTP(w, req)
|
||||||
|
|
||||||
|
assert.Equal(t, http.StatusBadRequest, w.Code, w.Body)
|
||||||
|
|
||||||
|
// error: more than one command
|
||||||
|
payload = rest.Command{
|
||||||
|
Race: "SomeRace",
|
||||||
|
Vote: &rest.CommandVote{
|
||||||
|
Recipient: "AnotherRace",
|
||||||
|
},
|
||||||
|
DeclarePeace: &rest.CommandDeclarePeace{
|
||||||
|
Opponent: "OpponentRace",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
w = httptest.NewRecorder()
|
||||||
|
req, _ = http.NewRequest("PUT", "/api/v1/command", asBody(payload))
|
||||||
|
r.ServeHTTP(w, req)
|
||||||
|
|
||||||
|
assert.Equal(t, http.StatusBadRequest, w.Code, w.Body)
|
||||||
|
}
|
||||||
@@ -1,26 +1,29 @@
|
|||||||
package router
|
package handler
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"errors"
|
"errors"
|
||||||
"net/http"
|
"net/http"
|
||||||
|
|
||||||
|
"github.com/iliadenisov/galaxy/internal/controller"
|
||||||
"github.com/iliadenisov/galaxy/internal/game"
|
"github.com/iliadenisov/galaxy/internal/game"
|
||||||
|
|
||||||
"github.com/gin-gonic/gin"
|
"github.com/gin-gonic/gin"
|
||||||
"github.com/iliadenisov/galaxy/internal/model/rest"
|
"github.com/iliadenisov/galaxy/internal/model/rest"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
type CommandExecutor func(controller.Config, rest.Command) error
|
||||||
|
|
||||||
var (
|
var (
|
||||||
ErrCommandNotProcessed = errors.New("command was not processed by executor")
|
ErrCommandNotProcessed = errors.New("command was not processed by executor")
|
||||||
)
|
)
|
||||||
|
|
||||||
func CommandHandler(c *gin.Context, executor Executor) {
|
func CommandHandler(c *gin.Context, config controller.Config, executor CommandExecutor) {
|
||||||
var cmd rest.Command
|
var cmd rest.Command
|
||||||
if err := c.ShouldBindJSON(&cmd); err != nil {
|
if err := c.ShouldBindJSON(&cmd); err != nil {
|
||||||
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
err := executor(cmd)
|
err := executor(config, cmd)
|
||||||
switch {
|
switch {
|
||||||
case err == nil:
|
case err == nil:
|
||||||
c.Status(http.StatusOK)
|
c.Status(http.StatusOK)
|
||||||
@@ -32,13 +35,12 @@ func CommandHandler(c *gin.Context, executor Executor) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func Execute(cmd rest.Command) error {
|
func ExecuteCommand(config controller.Config, cmd rest.Command) error {
|
||||||
p := param()
|
|
||||||
switch {
|
switch {
|
||||||
case cmd.DeclareWar != nil:
|
case cmd.DeclareWar != nil:
|
||||||
return game.DeclareWar(p, cmd.Race, cmd.DeclareWar.Opponent)
|
return game.DeclareWar(config, cmd.Race, cmd.DeclareWar.Opponent)
|
||||||
case cmd.DeclarePeace != nil:
|
case cmd.DeclarePeace != nil:
|
||||||
return game.DeclarePeace(p, cmd.Race, cmd.DeclareWar.Opponent)
|
return game.DeclarePeace(config, cmd.Race, cmd.DeclareWar.Opponent)
|
||||||
default:
|
default:
|
||||||
return ErrCommandNotProcessed
|
return ErrCommandNotProcessed
|
||||||
}
|
}
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
package router
|
package handler
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"errors"
|
"errors"
|
||||||
@@ -6,19 +6,11 @@ import (
|
|||||||
|
|
||||||
"github.com/gin-gonic/gin"
|
"github.com/gin-gonic/gin"
|
||||||
e "github.com/iliadenisov/galaxy/internal/error"
|
e "github.com/iliadenisov/galaxy/internal/error"
|
||||||
"github.com/iliadenisov/galaxy/internal/game"
|
|
||||||
"github.com/iliadenisov/galaxy/internal/model/rest"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
func StatusHandler(c *gin.Context) {
|
func transformError(c *gin.Context, err error) bool {
|
||||||
g, err := game.LoadState(param())
|
|
||||||
|
|
||||||
if err == nil {
|
if err == nil {
|
||||||
c.JSON(http.StatusOK, rest.Status{
|
return false
|
||||||
Turn: g.Age,
|
|
||||||
Players: len(g.Race),
|
|
||||||
})
|
|
||||||
return
|
|
||||||
}
|
}
|
||||||
|
|
||||||
var ge = new(e.GenericError)
|
var ge = new(e.GenericError)
|
||||||
@@ -30,8 +22,10 @@ func StatusHandler(c *gin.Context) {
|
|||||||
default:
|
default:
|
||||||
c.JSON(http.StatusInternalServerError, gin.H{"generic_error": ge.Error(), "code": ge.Code})
|
c.JSON(http.StatusInternalServerError, gin.H{"generic_error": ge.Error(), "code": ge.Code})
|
||||||
}
|
}
|
||||||
return
|
} else {
|
||||||
|
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
return true
|
||||||
}
|
}
|
||||||
@@ -0,0 +1,33 @@
|
|||||||
|
package handler
|
||||||
|
|
||||||
|
import (
|
||||||
|
"net/http"
|
||||||
|
|
||||||
|
"github.com/gin-gonic/gin"
|
||||||
|
"github.com/iliadenisov/galaxy/internal/controller"
|
||||||
|
"github.com/iliadenisov/galaxy/internal/game"
|
||||||
|
"github.com/iliadenisov/galaxy/internal/model/rest"
|
||||||
|
)
|
||||||
|
|
||||||
|
func InitHandler(c *gin.Context, config controller.Config) {
|
||||||
|
var init rest.Init
|
||||||
|
if err := c.ShouldBindJSON(&init); err != nil {
|
||||||
|
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
races := make([]string, len(init.Races))
|
||||||
|
for i := range init.Races {
|
||||||
|
races[i] = init.Races[i].Name
|
||||||
|
}
|
||||||
|
|
||||||
|
uuid, err := game.GenerateGame(config, races)
|
||||||
|
if transformError(c, err) {
|
||||||
|
return
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
c.JSON(http.StatusCreated, rest.InitResponse{
|
||||||
|
UUID: uuid,
|
||||||
|
})
|
||||||
|
}
|
||||||
@@ -0,0 +1,23 @@
|
|||||||
|
package handler
|
||||||
|
|
||||||
|
import (
|
||||||
|
"net/http"
|
||||||
|
|
||||||
|
"github.com/gin-gonic/gin"
|
||||||
|
"github.com/iliadenisov/galaxy/internal/controller"
|
||||||
|
"github.com/iliadenisov/galaxy/internal/game"
|
||||||
|
"github.com/iliadenisov/galaxy/internal/model/rest"
|
||||||
|
)
|
||||||
|
|
||||||
|
func StatusHandler(c *gin.Context, config controller.Config) {
|
||||||
|
g, err := game.LoadState(config)
|
||||||
|
|
||||||
|
if transformError(c, err) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
c.JSON(http.StatusOK, rest.Status{
|
||||||
|
Turn: g.Age,
|
||||||
|
Players: len(g.Race),
|
||||||
|
})
|
||||||
|
}
|
||||||
@@ -0,0 +1,55 @@
|
|||||||
|
package router_test
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"net/http"
|
||||||
|
"net/http/httptest"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/google/uuid"
|
||||||
|
"github.com/iliadenisov/galaxy/internal/controller"
|
||||||
|
"github.com/iliadenisov/galaxy/internal/model/rest"
|
||||||
|
"github.com/iliadenisov/galaxy/internal/router"
|
||||||
|
"github.com/iliadenisov/galaxy/internal/util"
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestInit(t *testing.T) {
|
||||||
|
root, cleanup := util.CreateWorkDir(t)
|
||||||
|
defer cleanup()
|
||||||
|
|
||||||
|
r := router.SetupRouterConfig(func(p *controller.Param) { p.StoragePath = root })
|
||||||
|
|
||||||
|
payload := generateInitRequest(10)
|
||||||
|
|
||||||
|
w := httptest.NewRecorder()
|
||||||
|
req, _ := http.NewRequest("POST", "/api/v1/init", asBody(payload))
|
||||||
|
r.ServeHTTP(w, req)
|
||||||
|
|
||||||
|
assert.Equal(t, http.StatusCreated, w.Code, w.Body)
|
||||||
|
var initResponse rest.InitResponse
|
||||||
|
assert.NoError(t, json.Unmarshal(w.Body.Bytes(), &initResponse))
|
||||||
|
assert.NoError(t, uuid.Validate(initResponse.UUID.String()))
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestInitValidators(t *testing.T) {
|
||||||
|
r := router.SetupRouter()
|
||||||
|
payload := generateInitRequest(9)
|
||||||
|
|
||||||
|
w := httptest.NewRecorder()
|
||||||
|
req, _ := http.NewRequest("POST", "/api/v1/init", asBody(payload))
|
||||||
|
r.ServeHTTP(w, req)
|
||||||
|
|
||||||
|
assert.Equal(t, http.StatusBadRequest, w.Code, w.Body)
|
||||||
|
}
|
||||||
|
|
||||||
|
func generateInitRequest(races int) rest.Init {
|
||||||
|
request := rest.Init{
|
||||||
|
Races: make([]rest.Race, races),
|
||||||
|
}
|
||||||
|
for i := range request.Races {
|
||||||
|
request.Races[i] = rest.Race{Name: fmt.Sprintf("Race_%02d", i)}
|
||||||
|
}
|
||||||
|
return request
|
||||||
|
}
|
||||||
+14
-20
@@ -7,29 +7,19 @@ import (
|
|||||||
"github.com/gin-gonic/gin/binding"
|
"github.com/gin-gonic/gin/binding"
|
||||||
"github.com/go-playground/validator/v10"
|
"github.com/go-playground/validator/v10"
|
||||||
"github.com/iliadenisov/galaxy/internal/controller"
|
"github.com/iliadenisov/galaxy/internal/controller"
|
||||||
"github.com/iliadenisov/galaxy/internal/model/rest"
|
"github.com/iliadenisov/galaxy/internal/router/handler"
|
||||||
)
|
)
|
||||||
|
|
||||||
var (
|
func initConfig() func(*controller.Param) {
|
||||||
StoragePath string
|
|
||||||
)
|
|
||||||
|
|
||||||
func init() {
|
|
||||||
StoragePath = os.Getenv("STORAGE_PATH")
|
|
||||||
}
|
|
||||||
|
|
||||||
func param() func(*controller.Param) {
|
|
||||||
return func(p *controller.Param) {
|
return func(p *controller.Param) {
|
||||||
// TODO: initialize base controller settings
|
// TODO: initialize base controller settings
|
||||||
p.StoragePath = StoragePath
|
p.StoragePath = os.Getenv("STORAGE_PATH")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
type Executor func(rest.Command) error
|
|
||||||
|
|
||||||
type Router struct {
|
type Router struct {
|
||||||
r *gin.Engine
|
r *gin.Engine
|
||||||
executor Executor
|
executor handler.CommandExecutor
|
||||||
}
|
}
|
||||||
|
|
||||||
func (r Router) Run() error {
|
func (r Router) Run() error {
|
||||||
@@ -37,14 +27,14 @@ func (r Router) Run() error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func NewRouter() Router {
|
func NewRouter() Router {
|
||||||
return NewRouterExecutor(Execute)
|
return NewRouterExecutor(handler.ExecuteCommand)
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewRouterExecutor(executor Executor) Router {
|
func NewRouterExecutor(executor handler.CommandExecutor) Router {
|
||||||
return Router{r: setupRouter(executor)}
|
return Router{r: setupRouter(initConfig(), executor)}
|
||||||
}
|
}
|
||||||
|
|
||||||
func setupRouter(executor Executor) *gin.Engine {
|
func setupRouter(config controller.Config, executor handler.CommandExecutor) *gin.Engine {
|
||||||
gin.SetMode(gin.ReleaseMode)
|
gin.SetMode(gin.ReleaseMode)
|
||||||
r := gin.New()
|
r := gin.New()
|
||||||
r.Use(gin.Recovery())
|
r.Use(gin.Recovery())
|
||||||
@@ -53,7 +43,11 @@ func setupRouter(executor Executor) *gin.Engine {
|
|||||||
v.RegisterValidation("notblank", notBlankStringValidator)
|
v.RegisterValidation("notblank", notBlankStringValidator)
|
||||||
}
|
}
|
||||||
|
|
||||||
r.GET("/api/v1/status", StatusHandler)
|
groupV1 := r.Group("/api/v1")
|
||||||
r.PUT("/api/v1/command", LimitMiddleware(1), func(ctx *gin.Context) { CommandHandler(ctx, executor) })
|
|
||||||
|
groupV1.GET("/status", func(ctx *gin.Context) { handler.StatusHandler(ctx, config) })
|
||||||
|
groupV1.POST("/init", func(ctx *gin.Context) { handler.InitHandler(ctx, config) })
|
||||||
|
groupV1.PUT("/command", LimitMiddleware(1), func(ctx *gin.Context) { handler.CommandHandler(ctx, config, executor) })
|
||||||
|
|
||||||
return r
|
return r
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,9 +2,14 @@ package router
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"github.com/gin-gonic/gin"
|
"github.com/gin-gonic/gin"
|
||||||
|
"github.com/iliadenisov/galaxy/internal/controller"
|
||||||
"github.com/iliadenisov/galaxy/internal/model/rest"
|
"github.com/iliadenisov/galaxy/internal/model/rest"
|
||||||
)
|
)
|
||||||
|
|
||||||
func SetupRouter() *gin.Engine {
|
func SetupRouter() *gin.Engine {
|
||||||
return setupRouter(func(c rest.Command) error { return nil })
|
return SetupRouterConfig(nil)
|
||||||
|
}
|
||||||
|
|
||||||
|
func SetupRouterConfig(config controller.Config) *gin.Engine {
|
||||||
|
return setupRouter(config, func(controller.Config, rest.Command) error { return nil })
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -10,71 +10,10 @@ import (
|
|||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"github.com/gin-gonic/gin"
|
"github.com/gin-gonic/gin"
|
||||||
"github.com/iliadenisov/galaxy/internal/model/rest"
|
|
||||||
"github.com/iliadenisov/galaxy/internal/router"
|
"github.com/iliadenisov/galaxy/internal/router"
|
||||||
"github.com/stretchr/testify/assert"
|
"github.com/stretchr/testify/assert"
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestRouter(t *testing.T) {
|
|
||||||
r := router.SetupRouter()
|
|
||||||
|
|
||||||
exampleCommand := rest.Command{
|
|
||||||
Race: "SomeRace",
|
|
||||||
Vote: &rest.CommandVote{
|
|
||||||
Recipient: "AnotherRace",
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
w := httptest.NewRecorder()
|
|
||||||
req, _ := http.NewRequest("PUT", "/api/v1/command", cmdBody(exampleCommand))
|
|
||||||
r.ServeHTTP(w, req)
|
|
||||||
|
|
||||||
assert.Equal(t, http.StatusOK, w.Code, w.Body)
|
|
||||||
|
|
||||||
// error: notblank validator
|
|
||||||
exampleCommand.Race = ""
|
|
||||||
w = httptest.NewRecorder()
|
|
||||||
req, _ = http.NewRequest("PUT", "/api/v1/command", cmdBody(exampleCommand))
|
|
||||||
r.ServeHTTP(w, req)
|
|
||||||
|
|
||||||
assert.Equal(t, http.StatusBadRequest, w.Code, w.Body)
|
|
||||||
|
|
||||||
exampleCommand.Race = " "
|
|
||||||
w = httptest.NewRecorder()
|
|
||||||
req, _ = http.NewRequest("PUT", "/api/v1/command", cmdBody(exampleCommand))
|
|
||||||
r.ServeHTTP(w, req)
|
|
||||||
|
|
||||||
assert.Equal(t, http.StatusBadRequest, w.Code, w.Body)
|
|
||||||
|
|
||||||
// error: no commands
|
|
||||||
exampleCommand = rest.Command{
|
|
||||||
Race: "SomeRace",
|
|
||||||
}
|
|
||||||
|
|
||||||
w = httptest.NewRecorder()
|
|
||||||
req, _ = http.NewRequest("PUT", "/api/v1/command", cmdBody(exampleCommand))
|
|
||||||
r.ServeHTTP(w, req)
|
|
||||||
|
|
||||||
assert.Equal(t, http.StatusBadRequest, w.Code, w.Body)
|
|
||||||
|
|
||||||
// error: more than one command
|
|
||||||
exampleCommand = rest.Command{
|
|
||||||
Race: "SomeRace",
|
|
||||||
Vote: &rest.CommandVote{
|
|
||||||
Recipient: "AnotherRace",
|
|
||||||
},
|
|
||||||
DeclarePeace: &rest.CommandDeclarePeace{
|
|
||||||
Opponent: "OpponentRace",
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
w = httptest.NewRecorder()
|
|
||||||
req, _ = http.NewRequest("PUT", "/api/v1/command", cmdBody(exampleCommand))
|
|
||||||
r.ServeHTTP(w, req)
|
|
||||||
|
|
||||||
assert.Equal(t, http.StatusBadRequest, w.Code, w.Body)
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestLimitConnections(t *testing.T) {
|
func TestLimitConnections(t *testing.T) {
|
||||||
r := limitTestingRouter()
|
r := limitTestingRouter()
|
||||||
|
|
||||||
@@ -94,8 +33,8 @@ func TestLimitConnections(t *testing.T) {
|
|||||||
wg.Wait()
|
wg.Wait()
|
||||||
}
|
}
|
||||||
|
|
||||||
func cmdBody(cmd rest.Command) *strings.Reader {
|
func asBody(body any) *strings.Reader {
|
||||||
commandJson, _ := json.Marshal(cmd)
|
commandJson, _ := json.Marshal(body)
|
||||||
return strings.NewReader(string(commandJson))
|
return strings.NewReader(string(commandJson))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -17,5 +17,4 @@ func TestGetStatus(t *testing.T) {
|
|||||||
r.ServeHTTP(w, req)
|
r.ServeHTTP(w, req)
|
||||||
|
|
||||||
assert.Equal(t, http.StatusNotImplemented, w.Code, w.Body)
|
assert.Equal(t, http.StatusNotImplemented, w.Code, w.Body)
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user