First commit

This commit is contained in:
2022-06-29 21:26:05 -04:00
commit e7559f0bf1
48 changed files with 8132 additions and 0 deletions

9
backend/.env.example Normal file
View File

@@ -0,0 +1,9 @@
MYSQL_DATABASE=alai
MYSQL_ROOT_PASSWORD=rootpass
MYSQL_USER=user
MYSQL_PASSWORD=pass
MYSQL_HOST=%
MYSQL_HOST_IP=localhost
MYSQL_PORT=3306
JWT_SECRET=supersecretkey
ADMIN_PASSWORD=adminpass

11
backend/Dockerfile Normal file
View File

@@ -0,0 +1,11 @@
FROM golang:1.18.3 AS builder
WORKDIR /usr/src/app
COPY go.mod go.sum ./
RUN go mod download && go mod verify
COPY . .
RUN go build -buildvcs=false -v -o /usr/local/bin/ ./...
FROM golang:1.18.3
WORKDIR /usr/local/bin
COPY --from=builder /usr/local/bin/backend .
CMD ["backend", "migrate", "serve"]

104
backend/controllers/game.go Normal file
View File

@@ -0,0 +1,104 @@
package controllers
import (
"backend/database"
"backend/models"
"backend/utils"
"compress/gzip"
"encoding/base64"
"encoding/json"
"net/http"
"github.com/julienschmidt/httprouter"
"gorm.io/gorm"
)
func PostGame(writer http.ResponseWriter, request *http.Request, params httprouter.Params) {
gdb := database.Connect()
defer database.Close(gdb)
decoded := base64.NewDecoder(base64.StdEncoding, request.Body)
uncompressed, err := gzip.NewReader(decoded)
if err != nil {
utils.JSONErrorOutput(writer, http.StatusBadRequest, err.Error())
return
}
defer uncompressed.Close()
decoder := json.NewDecoder(uncompressed)
var game models.Game
err = decoder.Decode(&game)
if err != nil {
utils.JSONErrorOutput(writer, http.StatusBadRequest, err.Error())
return
}
err = game.Validate()
if err != nil {
utils.JSONErrorOutput(writer, http.StatusBadRequest, err.Error())
return
}
result := postGame(game, gdb)
if result.Error != nil {
utils.JSONErrorOutput(writer, http.StatusBadRequest, result.Error.Error())
return
} else {
writer.WriteHeader(http.StatusOK)
return
}
}
func postGame(game models.Game, gdb *gorm.DB) *gorm.DB {
return gdb.Create(&game)
}
func ListGames(writer http.ResponseWriter, request *http.Request, params httprouter.Params) {
gdb := database.Connect()
defer database.Close(gdb)
var games []models.Game
result := listGames(&games, gdb)
if result.Error != nil {
utils.JSONErrorOutput(writer, http.StatusBadRequest, result.Error.Error())
return
} else {
writer.Header().Set("Content-Type", "application/json")
writer.WriteHeader(http.StatusOK)
json.NewEncoder(writer).Encode(games)
return
}
}
func listGames(games *[]models.Game, gdb *gorm.DB) *gorm.DB {
return gdb.Model(&models.Game{}).Order("ID asc").Joins("Player").Joins("Level").Joins("OS").Joins("GodotVersion").Find(&games)
}
func GetGame(writer http.ResponseWriter, request *http.Request, params httprouter.Params) {
gdb := database.Connect()
defer database.Close(gdb)
var game models.Game
result := getGame(&game, params.ByName("id"), gdb)
if result.Error != nil {
utils.JSONErrorOutput(writer, http.StatusBadRequest, result.Error.Error())
return
} else if result.RowsAffected == 0 {
utils.JSONErrorOutput(writer, http.StatusNotFound, "A game with the ID "+params.ByName("id")+" does not exist!")
return
} else {
writer.Header().Set("Content-Type", "application/json")
writer.WriteHeader(http.StatusOK)
json.NewEncoder(writer).Encode(game)
return
}
}
func getGame(games *models.Game, id string, gdb *gorm.DB) *gorm.DB {
return gdb.Model(&models.Game{}).Order("ID asc").Joins("Player").Joins("Level").Joins("OS").Find(&games, id)
}

134
backend/controllers/user.go Normal file
View File

@@ -0,0 +1,134 @@
package controllers
import (
"backend/database"
"backend/models"
"backend/utils"
"encoding/json"
"errors"
"net/http"
"strconv"
"github.com/julienschmidt/httprouter"
)
func AuthenticateUser(writer http.ResponseWriter, request *http.Request, params httprouter.Params) {
type Message struct {
Status string `json:"status"`
}
message := Message{Status: "authorized"}
writer.Header().Set("Content-Type", "application/json")
writer.WriteHeader(http.StatusOK)
json.NewEncoder(writer).Encode(message)
}
func CreateUser(writer http.ResponseWriter, request *http.Request, params httprouter.Params) {
gdb := database.Connect()
defer database.Close(gdb)
user := models.User{
Name: request.FormValue("name"),
Email: request.FormValue("email"),
}
user.HashPassword(request.FormValue("password"))
gdb.Create(&user)
}
func UpdateUser(writer http.ResponseWriter, request *http.Request, params httprouter.Params) {
gdb := database.Connect()
defer database.Close(gdb)
var user models.User
userID, _ := strconv.ParseUint(params.ByName("id"), 10, 64)
gdb.Model(models.User{}).Where(&models.User{ID: userID}).Find(&user)
if request.FormValue("password") != "" {
var oldPassword = request.FormValue("old_password")
err := user.CheckPassword(oldPassword)
if err != nil {
utils.JSONErrorOutput(writer, http.StatusBadRequest, errors.New("incorrect password").Error())
return
} else {
user.HashPassword(request.FormValue("password"))
}
}
user.Name = request.FormValue("name")
user.Email = request.FormValue("email")
gdb.Updates(&user)
}
func ListUsers(writer http.ResponseWriter, request *http.Request, params httprouter.Params) {
gdb := database.Connect()
defer database.Close(gdb)
var users []models.User
result := gdb.Model(&models.User{}).Order("ID asc").Find(&users)
if result.Error != nil {
utils.JSONErrorOutput(writer, http.StatusBadRequest, result.Error.Error())
return
} else {
for i := range users {
users[i].Password = ""
}
writer.Header().Set("Content-Type", "application/json")
writer.WriteHeader(http.StatusOK)
json.NewEncoder(writer).Encode(users)
}
}
func GetUser(writer http.ResponseWriter, request *http.Request, params httprouter.Params) {
gdb := database.Connect()
defer database.Close(gdb)
var user models.User
result := gdb.Model(&models.User{}).Find(&user, params.ByName("id"))
if result.Error != nil {
utils.JSONErrorOutput(writer, http.StatusBadRequest, result.Error.Error())
return
} else if result.RowsAffected == 0 {
utils.JSONErrorOutput(writer, http.StatusNotFound, "A user with the id "+params.ByName("id")+" doesn't exist!")
return
} else {
user.Password = ""
writer.Header().Set("Content-Type", "application/json")
writer.WriteHeader(http.StatusOK)
json.NewEncoder(writer).Encode(user)
}
}
func Login(writer http.ResponseWriter, request *http.Request, params httprouter.Params) {
gdb := database.Connect()
defer database.Close(gdb)
var user models.User
gdb.Model(models.User{}).Where(&models.User{Username: request.FormValue("username")}).Find(&user)
err := user.CheckPassword(request.FormValue("password"))
if err != nil {
utils.JSONErrorOutput(writer, http.StatusBadRequest, errors.New("incorrect password").Error())
return
}
type Token struct {
Token string `json:"token"`
}
tokenString, _ := utils.GenerateJWT("chris@cromer.cl", "cromer")
token := Token{Token: tokenString}
writer.Header().Set("Content-Type", "application/json")
writer.WriteHeader(http.StatusOK)
json.NewEncoder(writer).Encode(token)
}

View File

@@ -0,0 +1,132 @@
package database
import (
"backend/models"
"os"
"gorm.io/driver/mysql"
"gorm.io/gorm"
"gorm.io/gorm/schema"
)
func Connect() *gorm.DB {
dbUser := os.Getenv("MYSQL_USER")
dbPass := os.Getenv("MYSQL_PASSWORD")
dbHost := os.Getenv("MYSQL_HOST_IP")
dbName := os.Getenv("MYSQL_DATABASE")
gdb, err := gorm.Open(mysql.New(mysql.Config{
DSN: dbUser + ":" + dbPass + "@tcp(" + dbHost + ")/" + dbName + "?charset=utf8mb4&parseTime=True&loc=Local",
DefaultStringSize: 256,
}), &gorm.Config{
CreateBatchSize: 1000,
NamingStrategy: schema.NamingStrategy{
SingularTable: true,
},
})
if err != nil {
panic("failed to connect database")
}
return gdb
}
func AutoMigrate(gdb *gorm.DB) {
var populate = make(map[string]bool)
if (!gdb.Migrator().HasTable(&models.OS{})) {
populate["os"] = true
}
if (!gdb.Migrator().HasTable(&models.Level{})) {
populate["level"] = true
}
if (!gdb.Migrator().HasTable(&models.User{})) {
populate["user"] = true
}
gdb.AutoMigrate(&models.GodotVersion{})
gdb.AutoMigrate(&models.Game{})
gdb.AutoMigrate(&models.Level{})
gdb.AutoMigrate(&models.OS{})
gdb.AutoMigrate(&models.Player{})
gdb.AutoMigrate(&models.ObjectState{})
gdb.AutoMigrate(&models.ObjectName{})
gdb.AutoMigrate(&models.Object{})
gdb.AutoMigrate(&models.Frame{})
gdb.AutoMigrate(&models.User{})
Populate(gdb, populate)
}
func DropAll(gdb *gorm.DB) {
err := gdb.Migrator().DropTable(
&models.GodotVersion{},
&models.Object{},
&models.Frame{},
&models.Game{},
&models.Level{},
&models.OS{},
&models.Player{},
&models.ObjectState{},
&models.ObjectName{},
&models.User{})
if err != nil {
panic(err.Error())
}
}
func Populate(gdb *gorm.DB, populate map[string]bool) {
if _, ok := populate["os"]; ok {
os_list := []models.OS{
{Name: "Android"},
{Name: "iOS"},
{Name: "HTML5"},
{Name: "OSX"},
{Name: "Server"},
{Name: "Windows"},
{Name: "UWP"},
{Name: "X11"},
}
gdb.Create(&os_list)
}
if _, ok := populate["level"]; ok {
level_list := []models.Level{
{Name: "Prototype"},
{Name: "PrototypeR"},
{Name: "Level1"},
{Name: "Level2"},
{Name: "Level3"},
{Name: "Level4"},
{Name: "Level5"},
{Name: "Level6"},
}
gdb.Create(&level_list)
}
if _, ok := populate["user"]; ok {
user_list := []models.User{
{
Name: "Chris Cromer",
Username: "cromer",
Email: "chris@cromer.cl",
},
{
Name: "Martín Araneda",
Username: "martin",
Email: "martix.araneda@gmail.com",
},
}
user_list[0].HashPassword(os.Getenv("ADMIN_PASSWORD"))
user_list[1].HashPassword(os.Getenv("ADMIN_PASSWORD"))
gdb.Create(&user_list)
}
}
func Close(gdb *gorm.DB) {
sqlDB, err := gdb.DB()
if err != nil {
panic("failed to get gorm db")
}
sqlDB.Close()
}

24
backend/go.mod Normal file
View File

@@ -0,0 +1,24 @@
module backend
go 1.18
require github.com/julienschmidt/httprouter v1.3.0
require golang.org/x/crypto v0.0.0-20220525230936-793ad666bf5e
require github.com/joho/godotenv v1.4.0
require github.com/golang-jwt/jwt/v4 v4.4.2
require (
github.com/go-sql-driver/mysql v1.6.0 // indirect
github.com/gorilla/handlers v1.5.1
)
require (
github.com/felixge/httpsnoop v1.0.3 // indirect
github.com/jinzhu/inflection v1.0.0 // indirect
github.com/jinzhu/now v1.1.5 // indirect
gorm.io/driver/mysql v1.3.4
gorm.io/gorm v1.23.6
)

25
backend/go.sum Normal file
View File

@@ -0,0 +1,25 @@
github.com/felixge/httpsnoop v1.0.1/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U=
github.com/felixge/httpsnoop v1.0.3 h1:s/nj+GCswXYzN5v2DpNMuMQYe+0DDwt5WVCU6CWBdXk=
github.com/felixge/httpsnoop v1.0.3/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U=
github.com/go-sql-driver/mysql v1.6.0 h1:BCTh4TKNUYmOmMUcQ3IipzF5prigylS7XXjEkfCHuOE=
github.com/go-sql-driver/mysql v1.6.0/go.mod h1:DCzpHaOWr8IXmIStZouvnhqoel9Qv2LBy8hT2VhHyBg=
github.com/golang-jwt/jwt/v4 v4.4.2 h1:rcc4lwaZgFMCZ5jxF9ABolDcIHdBytAFgqFPbSJQAYs=
github.com/golang-jwt/jwt/v4 v4.4.2/go.mod h1:m21LjoU+eqJr34lmDMbreY2eSTRJ1cv77w39/MY0Ch0=
github.com/gorilla/handlers v1.5.1 h1:9lRY6j8DEeeBT10CvO9hGW0gmky0BprnvDI5vfhUHH4=
github.com/gorilla/handlers v1.5.1/go.mod h1:t8XrUpc4KVXb7HGyJ4/cEnwQiaxrX/hz1Zv/4g96P1Q=
github.com/jinzhu/inflection v1.0.0 h1:K317FqzuhWc8YvSVlFMCCUb36O/S9MCKRDI7QkRKD/E=
github.com/jinzhu/inflection v1.0.0/go.mod h1:h+uFLlag+Qp1Va5pdKtLDYj+kHp5pxUVkryuEj+Srlc=
github.com/jinzhu/now v1.1.4/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8=
github.com/jinzhu/now v1.1.5 h1:/o9tlHleP7gOFmsnYNz3RGnqzefHA47wQpKrrdTIwXQ=
github.com/jinzhu/now v1.1.5/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8=
github.com/joho/godotenv v1.4.0 h1:3l4+N6zfMWnkbPEXKng2o2/MR5mSwTrBih4ZEkkz1lg=
github.com/joho/godotenv v1.4.0/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4=
github.com/julienschmidt/httprouter v1.3.0 h1:U0609e9tgbseu3rBINet9P48AI/D3oJs4dN7jwJOQ1U=
github.com/julienschmidt/httprouter v1.3.0/go.mod h1:JR6WtHb+2LUe8TCKY3cZOxFyyO8IZAc4RVcycCCAKdM=
golang.org/x/crypto v0.0.0-20220525230936-793ad666bf5e h1:T8NU3HyQ8ClP4SEE+KbFlg6n0NhuTsN4MyznaarGsZM=
golang.org/x/crypto v0.0.0-20220525230936-793ad666bf5e/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
gorm.io/driver/mysql v1.3.4 h1:/KoBMgsUHC3bExsekDcmNYaBnfH2WNeFuXqqrqMc98Q=
gorm.io/driver/mysql v1.3.4/go.mod h1:s4Tq0KmD0yhPGHbZEwg1VPlH0vT/GBHJZorPzhcxBUE=
gorm.io/gorm v1.23.4/go.mod h1:l2lP/RyAtc1ynaTjFksBde/O8v9oOGIApu2/xRitmZk=
gorm.io/gorm v1.23.6 h1:KFLdNgri4ExFFGTRGGFWON2P1ZN28+9SJRN8voOoYe0=
gorm.io/gorm v1.23.6/go.mod h1:l2lP/RyAtc1ynaTjFksBde/O8v9oOGIApu2/xRitmZk=

72
backend/main.go Normal file
View File

@@ -0,0 +1,72 @@
package main
import (
"backend/database"
"backend/routes"
"fmt"
"os"
"github.com/joho/godotenv"
"gorm.io/gorm"
)
func main() {
err := godotenv.Load()
if err != nil {
print(err.Error())
}
allArgs := os.Args[1:]
dropDB := false
migrateDB := false
serve := false
dbOperation := false
for _, element := range allArgs {
if element == "drop" {
dropDB = true
dbOperation = true
}
if element == "migrate" {
migrateDB = true
dbOperation = true
}
if element == "serve" {
serve = true
}
}
var gdb *gorm.DB
if dbOperation {
fmt.Print("Connecting to database ... ")
gdb = database.Connect()
fmt.Println("DONE")
}
if dropDB {
fmt.Print("Dropping database ... ")
database.DropAll(gdb)
fmt.Println("DONE")
}
if migrateDB {
fmt.Print("AutoMigrating database ... ")
database.AutoMigrate(gdb)
fmt.Println("DONE")
}
if dbOperation {
fmt.Print("Closing database ... ")
database.Close(gdb)
fmt.Println("DONE")
}
if serve {
router := routes.Initialize()
fmt.Println("Serving routes ... DONE")
routes.Serve(router)
}
}

View File

@@ -0,0 +1,30 @@
package middlewares
import (
"backend/utils"
"errors"
"net/http"
"strings"
"github.com/julienschmidt/httprouter"
)
func Authenticate(handle httprouter.Handle) httprouter.Handle {
return func(writer http.ResponseWriter, request *http.Request, params httprouter.Params) {
reqToken := request.Header.Get("Authorization")
splitToken := strings.Split(reqToken, "Bearer ")
if len(splitToken) < 2 {
utils.JSONErrorOutput(writer, http.StatusUnauthorized, errors.New("no token received").Error())
return
}
tokenString := splitToken[1]
err := utils.ValidateToken(tokenString)
if err != nil {
utils.JSONErrorOutput(writer, http.StatusUnauthorized, err.Error())
return
}
handle(writer, request, params)
}
}

17
backend/models/frame.go Normal file
View File

@@ -0,0 +1,17 @@
package models
import (
"gorm.io/gorm"
)
type Frame struct {
gorm.Model
ID uint64 `json:"ID" gorm:"primaryKey"`
GameID uint64 `json:"game_id" gorm:"not null"`
Game Game `json:"game" gorm:"not null"`
Coins uint64 `json:"coins" gorm:";not null"`
Points uint64 `json:"points" gorm:"not null"`
FPS uint8 `json:"fps" gorm:"not null"`
ElapsedTime uint64 `json:"elapsed_time" gorm:"not null"`
Objects []Object `json:"objects"`
}

100
backend/models/game.go Normal file
View File

@@ -0,0 +1,100 @@
package models
import (
"errors"
"strings"
"gorm.io/gorm"
)
type Game struct {
gorm.Model
ID uint64 `json:"ID" gorm:"primaryKey"`
PlayerID uint64 `json:"player_id" gorm:"not null"`
Player Player `json:"player"`
LevelID uint64 `json:"level_id" gorm:"not null"`
Level Level `json:"level" gorm:"not null"`
OSID uint64 `json:"os_id" gorm:"not null"`
OS OS `json:"os" gorm:"not null"`
GodotVersionID uint64 `json:"godot_version_id" gorm:"not null"`
GodotVersion GodotVersion `json:"godot_version" gorm:"not null"`
ProcessorCount uint64 `json:"processor_count" gorm:"not null"`
ScreenCount uint8 `json:"screen_count" gorm:"not null"`
ScreenDPI uint8 `json:"screen_dpi" gorm:"not null"`
ScreenSize string `json:"screen_size" gorm:"not null"`
MachineId string `json:"machine_id" gorm:"not null"`
Locale string `json:"locale" gorm:"not null"`
GameVersion string `json:"game_version" gorm:"not null"`
Won bool `json:"won" gorm:"not null"`
Timestamp uint64 `json:"timestamp" gorm:"not null"`
Frames []Frame `json:"frames"`
}
func (game *Game) Validate() error {
if len(strings.TrimSpace(game.MachineId)) == 0 {
return errors.New("empty machine id")
}
return nil
}
// Cache the results of the queries here
// The object states and names should not be deleted so this should always be valid while running
var cachedStateNames = make(map[string]uint64)
var cachedObjectNames = make(map[string]uint64)
func (game *Game) BeforeCreate(tx *gorm.DB) error {
// Use the same player ID if the RUT is already in the DB
tx.Model(Player{}).Where(&Player{RUT: game.Player.RUT}).Find(&game.Player)
tx.Model(GodotVersion{}).Where(&GodotVersion{String: game.GodotVersion.String}).Find(&game.GodotVersion)
for frameIndex, frame := range game.Frames {
for objectIndex := range frame.Objects {
game.Frames[frameIndex].Objects[objectIndex].ObjectState.Name = game.Frames[frameIndex].Objects[objectIndex].State
game.Frames[frameIndex].Objects[objectIndex].ObjectName.Name = game.Frames[frameIndex].Objects[objectIndex].Name
// Use the existing state names in the database if they exist
if ID, ok := cachedStateNames[game.Frames[frameIndex].Objects[objectIndex].ObjectState.Name]; ok {
// The name is cached, no need to query the database
game.Frames[frameIndex].Objects[objectIndex].ObjectStateID = ID
game.Frames[frameIndex].Objects[objectIndex].ObjectState.ID = ID
} else {
var state ObjectState
result := tx.Model(ObjectState{}).Where(&ObjectState{Name: game.Frames[frameIndex].Objects[objectIndex].ObjectState.Name}).Find(&state)
if result.RowsAffected == 0 {
// Not in the database, so let's create it
tx.Create(&game.Frames[frameIndex].Objects[objectIndex].ObjectState)
game.Frames[frameIndex].Objects[objectIndex].ObjectStateID = game.Frames[frameIndex].Objects[objectIndex].ObjectState.ID
cachedStateNames[game.Frames[frameIndex].Objects[objectIndex].ObjectState.Name] = game.Frames[frameIndex].Objects[objectIndex].ObjectState.ID
} else {
// It is in the database, so use that
game.Frames[frameIndex].Objects[objectIndex].ObjectStateID = state.ID
game.Frames[frameIndex].Objects[objectIndex].ObjectState.ID = state.ID
cachedStateNames[game.Frames[frameIndex].Objects[objectIndex].ObjectState.Name] = state.ID
}
}
// Use the existing object names in the database if they exist
if ID, ok := cachedObjectNames[game.Frames[frameIndex].Objects[objectIndex].ObjectName.Name]; ok {
// The name is cached, no need to query the database
game.Frames[frameIndex].Objects[objectIndex].ObjectNameID = ID
game.Frames[frameIndex].Objects[objectIndex].ObjectName.ID = ID
} else {
var objectName ObjectName
result := tx.Model(ObjectName{}).Where(&ObjectName{Name: game.Frames[frameIndex].Objects[objectIndex].ObjectName.Name}).Find(&objectName)
if result.RowsAffected == 0 {
// Not in the database, so let's create it
tx.Create(&game.Frames[frameIndex].Objects[objectIndex].ObjectName)
game.Frames[frameIndex].Objects[objectIndex].ObjectNameID = game.Frames[frameIndex].Objects[objectIndex].ObjectName.ID
cachedObjectNames[game.Frames[frameIndex].Objects[objectIndex].ObjectName.Name] = game.Frames[frameIndex].Objects[objectIndex].ObjectName.ID
} else {
// It is in the database, so use that
game.Frames[frameIndex].Objects[objectIndex].ObjectNameID = objectName.ID
game.Frames[frameIndex].Objects[objectIndex].ObjectName.ID = objectName.ID
cachedObjectNames[game.Frames[frameIndex].Objects[objectIndex].ObjectName.Name] = objectName.ID
}
}
}
}
return nil
}

View File

@@ -0,0 +1,17 @@
package models
import "gorm.io/gorm"
type GodotVersion struct {
gorm.Model
ID uint64 `json:"ID" gorm:"primaryKey"`
Major uint8 `json:"major" gorm:"not null"`
Minor uint8 `json:"minor" gorm:"not null"`
Patch uint8 `json:"patch" gorm:"not null"`
Hex uint64 `json:"hex" gorm:"not null"`
Status string `json:"status" gorm:"not null"`
Build string `json:"build" gorm:"not null"`
Year uint16 `json:"year" gorm:"not null"`
Hash string `json:"hash" gorm:"unique;size:40;not null"`
String string `json:"string" gorm:"unique;not null"`
}

9
backend/models/level.go Normal file
View File

@@ -0,0 +1,9 @@
package models
import "gorm.io/gorm"
type Level struct {
gorm.Model
ID uint64 `json:"ID" gorm:"primaryKey"`
Name string `json:"rut" gorm:"unique;not null"`
}

20
backend/models/object.go Normal file
View File

@@ -0,0 +1,20 @@
package models
import "gorm.io/gorm"
type Object struct {
gorm.Model
ID uint64 `json:"ID" gorm:"primaryKey"`
FrameID uint64 `json:"frame_id" gorm:"not null"`
Frame Frame `json:"frame" gorm:"not null"`
ObjectNameID uint64 `json:"-" gorm:"not null"`
ObjectName ObjectName `json:"-" gorm:"not null"`
ObjectStateID uint64 `json:"-" gorm:"not null"`
ObjectState ObjectState `json:"-" gorm:"not null"`
Name string `json:"name" gorm:"-:all"`
State string `json:"state" gorm:"-:all"`
PositionX float64 `json:"position_x" gorm:"not null"`
PositionY float64 `json:"position_y" gorm:"not null"`
VelocityX float64 `json:"velocity_x" gorm:"not null"`
VelocityY float64 `json:"velocity_y" gorm:"not null"`
}

View File

@@ -0,0 +1,9 @@
package models
import "gorm.io/gorm"
type ObjectName struct {
gorm.Model
ID uint64 `json:"ID" gorm:"primaryKey"`
Name string `json:"name" gorm:"unique;not null"`
}

View File

@@ -0,0 +1,9 @@
package models
import "gorm.io/gorm"
type ObjectState struct {
gorm.Model
ID uint64 `json:"ID" gorm:"primaryKey"`
Name string `json:"name" gorm:"unique;not null"`
}

9
backend/models/os.go Normal file
View File

@@ -0,0 +1,9 @@
package models
import "gorm.io/gorm"
type OS struct {
gorm.Model
ID uint64 `json:"ID" gorm:"primaryKey"`
Name string `json:"name" gorm:"unique;size:8;not null"`
}

11
backend/models/player.go Normal file
View File

@@ -0,0 +1,11 @@
package models
import "gorm.io/gorm"
type Player struct {
gorm.Model
ID uint64 `json:"ID" gorm:"primaryKey"`
RUT string `json:"rut" gorm:"unique;size:9;not null"`
Name string `json:"name" gorm:"not null"`
Email string `json:"email" gorm:"unique;not null"`
}

32
backend/models/user.go Normal file
View File

@@ -0,0 +1,32 @@
package models
import (
"golang.org/x/crypto/bcrypt"
"gorm.io/gorm"
)
type User struct {
gorm.Model
ID uint64 `json:"ID" gorm:"primaryKey"`
Name string `json:"name" gorm:"not null"`
Username string `json:"username" gorm:"unique; not null"`
Email string `json:"email" gorm:"unique;not null"`
Password string `json:"password" gorm:"not null"`
}
func (user *User) HashPassword(password string) error {
bytes, err := bcrypt.GenerateFromPassword([]byte(password), 14)
if err != nil {
return err
}
user.Password = string(bytes)
return nil
}
func (user *User) CheckPassword(providedPassword string) error {
err := bcrypt.CompareHashAndPassword([]byte(user.Password), []byte(providedPassword))
if err != nil {
return err
}
return nil
}

14
backend/routes/game.go Normal file
View File

@@ -0,0 +1,14 @@
package routes
import (
"backend/controllers"
"backend/middlewares"
"github.com/julienschmidt/httprouter"
)
func GameRoutes(router *httprouter.Router) {
router.POST("/game", controllers.PostGame)
router.GET("/games", middlewares.Authenticate(controllers.ListGames))
router.GET("/game/:id", middlewares.Authenticate(controllers.GetGame))
}

56
backend/routes/router.go Normal file
View File

@@ -0,0 +1,56 @@
package routes
import (
"context"
"fmt"
"log"
"net/http"
"os"
"os/signal"
"syscall"
"github.com/gorilla/handlers"
"github.com/julienschmidt/httprouter"
)
func Initialize() *httprouter.Router {
router := httprouter.New()
router.GET("/", index)
UserRoutes(router)
GameRoutes(router)
return router
}
func Serve(router *httprouter.Router) {
newRouter := handlers.CombinedLoggingHandler(os.Stdout, router)
newRouter = handlers.CompressHandler(newRouter)
idleConnsClosed := make(chan struct{})
server := &http.Server{Addr: ":3001", Handler: newRouter}
// Listen for CTRL-C(SIGTERM)
sigterm := make(chan os.Signal)
signal.Notify(sigterm, os.Interrupt, syscall.SIGTERM)
go func() {
<-sigterm
// When CTRL-C is pressed shutdown the server
if err := server.Shutdown(context.Background()); err != nil {
log.Printf("HTTP server Shutdown: %v", err)
}
close(idleConnsClosed)
os.Exit(0)
}()
// Run the server
if err := server.ListenAndServe(); err != http.ErrServerClosed {
log.Fatalf("HTTP server ListenAndServe: %v", err)
}
<-idleConnsClosed
}
func index(writer http.ResponseWriter, _ *http.Request, _ httprouter.Params) {
fmt.Fprintf(writer, "This is the Alai API server!")
}

17
backend/routes/user.go Normal file
View File

@@ -0,0 +1,17 @@
package routes
import (
"backend/controllers"
"backend/middlewares"
"github.com/julienschmidt/httprouter"
)
func UserRoutes(router *httprouter.Router) {
router.POST("/login", controllers.Login)
router.GET("/users", middlewares.Authenticate(controllers.ListUsers))
router.POST("/user", middlewares.Authenticate(controllers.CreateUser))
router.GET("/user/:id", middlewares.Authenticate(controllers.GetUser))
router.PATCH("/user/:id", middlewares.Authenticate(controllers.UpdateUser))
router.GET("/auth", middlewares.Authenticate(controllers.AuthenticateUser))
}

3
backend/test.sh Executable file
View File

@@ -0,0 +1,3 @@
#!/bin/bash
go test -coverprofile=coverage.out ./...
go tool cover -html=coverage.out -o test_coverage.html

96
backend/utils/datatype.go Normal file
View File

@@ -0,0 +1,96 @@
package utils
import (
"database/sql"
"database/sql/driver"
"encoding/json"
"strings"
"time"
)
type Date time.Time // 2006-01-02
type DateTime time.Time // 2006-01-02 15:04:05
func (date *Date) UnmarshalJSON(b []byte) error {
s := strings.Trim(string(b), "\"")
t, err := time.Parse("2006-01-02", s)
if err != nil {
return err
}
*date = Date(t)
return nil
}
func (date Date) MarshalJSON() ([]byte, error) {
return json.Marshal(time.Time(date))
}
func (date Date) Format(s string) string {
t := time.Time(date)
return t.Format(s)
}
func (date *Date) Scan(value interface{}) (err error) {
nullTime := &sql.NullTime{}
err = nullTime.Scan(value)
*date = Date(nullTime.Time)
return
}
func (date Date) Value() (driver.Value, error) {
return time.Time(date), nil
}
func (date Date) GormDataType() string {
return "date"
}
func (date Date) GobEncode() ([]byte, error) {
return time.Time(date).GobEncode()
}
func (date *Date) GobDecode(b []byte) error {
return (*time.Time)(date).GobDecode(b)
}
func (dateTime *DateTime) UnmarshalJSON(b []byte) error {
s := strings.Trim(string(b), "\"")
t, err := time.Parse("2006-01-02 15:04:05", s)
if err != nil {
return err
}
*dateTime = DateTime(t)
return nil
}
func (dateTime DateTime) MarshalJSON() ([]byte, error) {
return json.Marshal(time.Time(dateTime))
}
func (dateTime DateTime) Format(s string) string {
t := time.Time(dateTime)
return t.Format(s)
}
func (date *DateTime) Scan(value interface{}) (err error) {
nullTime := &sql.NullTime{}
err = nullTime.Scan(value)
*date = DateTime(nullTime.Time)
return
}
func (date DateTime) Value() (driver.Value, error) {
return time.Time(date), nil
}
func (date DateTime) GormDataType() string {
return "datetime"
}
func (date DateTime) GobEncode() ([]byte, error) {
return time.Time(date).GobEncode()
}
func (date *DateTime) GobDecode(b []byte) error {
return (*time.Time)(date).GobDecode(b)
}

View File

@@ -0,0 +1,191 @@
package utils
import (
"reflect"
"testing"
"time"
)
func TestDateUnmarshalJSON(t *testing.T) {
want := "1985-02-23 00:00:00 +0000 UTC"
var date Date
err := date.UnmarshalJSON([]byte("\"1985-02-23\""))
msg := time.Time(date).String()
if msg != want {
t.Fatalf(`date.UnmarshalJSON([]byte("\"1985-02-23\"") = %q, %v, want match for %#q, nil`, msg, err, want)
}
}
func TestDateUnmarshalJSONMalformed(t *testing.T) {
want := error(&time.ParseError{})
var date Date
err := date.UnmarshalJSON([]byte("\"1985/02/23\""))
msg := time.Time(date).String()
if reflect.TypeOf(err) != reflect.TypeOf(want) {
t.Fatalf(`date.UnmarshalJSON([]byte("\"1985/02/23\"") = %q, %v, want match for %#q, nil`, msg, err, want)
}
}
func TestDateMarshalJSON(t *testing.T) {
want := "\"1985-02-23T00:00:00Z\""
parsed, _ := time.Parse("2006-01-02", "1985-02-23")
var date Date = Date(parsed)
msg, err := date.MarshalJSON()
if string(msg) != want {
t.Fatalf(`date.MarshalJSON() = %q, %v, want match for %#q, nil`, msg, err, want)
}
}
func TestDateFormat(t *testing.T) {
want := "Feb 23, 1985"
parsed, _ := time.Parse("2006-01-02", "1985-02-23")
var date Date = Date(parsed)
msg := date.Format("Jan 2, 2006")
if msg != want {
t.Fatalf(`date.Format("Jan 2, 2006") = %q, want match for %#q, nil`, msg, want)
}
}
func TestDateScan(t *testing.T) {
want := "0001-01-01T00:00:00Z"
parsed, _ := time.Parse("2006-01-02", "1985-02-23")
var date Date = Date(parsed)
err := date.Scan(Date{})
msg := date.Format("2006-01-02T15:04:05Z07:00")
if msg != want {
t.Fatalf(`date.Scan(Date{}) = %q, %v, want match for %#q, nil`, msg, err, want)
}
}
func TestDateValue(t *testing.T) {
var want time.Time
parsed, _ := time.Parse("2006-01-02", "1985-02-23")
var date Date = Date(parsed)
msg, _ := date.Value()
if reflect.TypeOf(msg) != reflect.TypeOf(want) {
t.Fatalf(`date.Value() = %q, want match for %#q, nil`, msg, want)
}
}
func TestDateGormDateType(t *testing.T) {
want := "date"
parsed, _ := time.Parse("2006-01-02", "1985-02-23")
var date Date = Date(parsed)
msg := date.GormDataType()
if msg != want {
t.Fatalf(`date.GormDateType() = %q, want match for %#q, nil`, msg, want)
}
}
func TestDateGobEncode(t *testing.T) {
want := "\x01\x00\x00\x00\x0e\x94\x0f!\x00\x00\x00\x00\x00\xff\xff"
parsed, _ := time.Parse("2006-01-02", "1985-02-23")
var date Date = Date(parsed)
msg, err := date.GobEncode()
if string(msg) != want {
t.Fatalf(`date.GobEncode() = %q, %v, want match for %#q, nil`, msg, err, want)
}
}
func TestDateGobDecode(t *testing.T) {
want := "1985-02-23T00:00:00Z"
parsed, _ := time.Parse("2006-01-02", "2006-01-02")
var date Date = Date(parsed)
err := date.GobDecode([]byte("\x01\x00\x00\x00\x0e\x94\x0f!\x00\x00\x00\x00\x00\xff\xff"))
msg := date.Format("2006-01-02T15:04:05Z07:00")
if string(msg) != want {
t.Fatalf(`date.GobDecode([]byte()) = %q, %v, want match for %#q, nil`, msg, err, want)
}
}
func TestDateTimeUnmarshalJSON(t *testing.T) {
want := "1985-02-23 12:13:14 +0000 UTC"
var dateTime DateTime
err := dateTime.UnmarshalJSON([]byte("\"1985-02-23 12:13:14\""))
msg := time.Time(dateTime).String()
if msg != want {
t.Fatalf(`dateTime.UnmarshalJSON([]byte("\"1985-02-23 12:13:14\"") = %q, %v, want match for %#q, nil`, msg, err, want)
}
}
func TestDateTimeUnmarshalJSONMalformed(t *testing.T) {
want := error(&time.ParseError{})
var dateTime DateTime
err := dateTime.UnmarshalJSON([]byte("\"1985/02/23 12-13-14\""))
msg := time.Time(dateTime).String()
if reflect.TypeOf(err) != reflect.TypeOf(want) {
t.Fatalf(`dateTime.UnmarshalJSON([]byte("\"1985/02/23 12-13-14\"") = %q, %v, want match for %#q, nil`, msg, err, want)
}
}
func TestDateTimeMarshalJSON(t *testing.T) {
want := "\"1985-02-23T12:13:14Z\""
parsed, _ := time.Parse("2006-01-02 15:04:05", "1985-02-23 12:13:14")
var dateTime DateTime = DateTime(parsed)
msg, err := dateTime.MarshalJSON()
if string(msg) != want {
t.Fatalf(`dateTime.MarshalJSON() = %q, %v, want match for %#q, nil`, msg, err, want)
}
}
func TestDateTimeFormat(t *testing.T) {
want := "Feb 23, 1985 12:13:14"
parsed, _ := time.Parse("2006-01-02 15:04:05", "1985-02-23 12:13:14")
var dateTime DateTime = DateTime(parsed)
msg := dateTime.Format("Jan 2, 2006 15:04:05")
if msg != want {
t.Fatalf(`dateTime.Format("Jan 2, 2006 15:04:05") = %q, want match for %#q, nil`, msg, want)
}
}
func TestDateTimeScan(t *testing.T) {
want := "0001-01-01T00:00:00Z"
parsed, _ := time.Parse("2006-01-02 15:04:05", "1985-02-23 12:13:14")
var dateTime DateTime = DateTime(parsed)
err := dateTime.Scan(Date{})
msg := dateTime.Format("2006-01-02T15:04:05Z07:00")
if msg != want {
t.Fatalf(`dateTime.Scan(Date{}) = %q, %v, want match for %#q, nil`, msg, err, want)
}
}
func TestDateTimeValue(t *testing.T) {
var want time.Time
parsed, _ := time.Parse("2006-01-02 15:04:05", "1985-02-23 12:13:14")
var dateTime DateTime = DateTime(parsed)
msg, _ := dateTime.Value()
if reflect.TypeOf(msg) != reflect.TypeOf(want) {
t.Fatalf(`dateTime.Value() = %q, want match for %#q, nil`, msg, want)
}
}
func TestDateTimeGormDateType(t *testing.T) {
want := "datetime"
parsed, _ := time.Parse("2006-01-02 15:04:05", "1985-02-23 12:13:14")
var dateTime DateTime = DateTime(parsed)
msg := dateTime.GormDataType()
if msg != want {
t.Fatalf(`dateTime.GormDateType() = %q, want match for %#q, nil`, msg, want)
}
}
func TestDateTimeGobEncode(t *testing.T) {
want := "\x01\x00\x00\x00\x0e\x94\x0f\xcc\xda\x00\x00\x00\x00\xff\xff"
parsed, _ := time.Parse("2006-01-02 15:04:05", "1985-02-23 12:13:14")
var dateTime DateTime = DateTime(parsed)
msg, err := dateTime.GobEncode()
if string(msg) != want {
t.Fatalf(`dateTime.GobEncode() = %q, %v, want match for %#q, nil`, msg, err, want)
}
}
func TestDateTimeGobDecode(t *testing.T) {
want := "1985-02-23T12:13:14Z"
parsed, _ := time.Parse("2006-01-02 15:04:05", "2006-01-02 12:13:14")
var dateTime DateTime = DateTime(parsed)
err := dateTime.GobDecode([]byte("\x01\x00\x00\x00\x0e\x94\x0f\xcc\xda\x00\x00\x00\x00\xff\xff"))
msg := dateTime.Format("2006-01-02T15:04:05Z07:00")
if string(msg) != want {
t.Fatalf(`dateTime.GobDecode([]byte()) = %q, %v, want match for %#q, nil`, msg, err, want)
}
}

16
backend/utils/json.go Normal file
View File

@@ -0,0 +1,16 @@
package utils
import (
"encoding/json"
"net/http"
)
type ErrorMessage struct {
ErrorMessage string `json:"error_message"`
}
func JSONErrorOutput(writer http.ResponseWriter, status int, msg string) {
writer.Header().Set("Content-Type", "application/json")
writer.WriteHeader(status)
json.NewEncoder(writer).Encode(ErrorMessage{ErrorMessage: msg})
}

52
backend/utils/jwt.go Normal file
View File

@@ -0,0 +1,52 @@
package utils
import (
"errors"
"os"
"time"
"github.com/golang-jwt/jwt/v4"
)
type JWTClaim struct {
Username string `json:"username"`
Email string `json:"email"`
jwt.RegisteredClaims
}
func GenerateJWT(email string, username string) (tokenString string, err error) {
expirationTime := time.Now().Add(1 * time.Hour)
claims := &JWTClaim{
Email: email,
Username: username,
RegisteredClaims: jwt.RegisteredClaims{
ExpiresAt: jwt.NewNumericDate(expirationTime),
},
}
token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims)
tokenString, err = token.SignedString(os.Getenv("JWT_SECRET"))
return
}
func ValidateToken(signedToken string) (err error) {
token, err := jwt.ParseWithClaims(
signedToken,
&JWTClaim{},
func(token *jwt.Token) (interface{}, error) {
return []byte(os.Getenv("JWT_SECRET")), nil
},
)
if err != nil {
return
}
claims, ok := token.Claims.(*JWTClaim)
if !ok {
err = errors.New("couldn't parse claims")
return
}
if claims.ExpiresAt.Unix() < jwt.NewNumericDate(time.Now().Local()).Unix() {
err = errors.New("token expired")
return
}
return
}

115
backend/utils/rut.go Normal file
View File

@@ -0,0 +1,115 @@
package utils
import (
"errors"
"strconv"
"strings"
)
type RutType int8
const (
Run RutType = iota
Rut
)
func (r RutType) String() string {
switch r {
case Run:
return "RUN"
case Rut:
return "RUT"
default:
return "unknown"
}
}
func CleanRut(rut *string) {
*rut = strings.ToUpper(*rut)
*rut = strings.TrimSpace(*rut)
*rut = strings.Replace(*rut, ".", "", -1)
*rut = strings.Replace(*rut, "-", "", -1)
}
func PrettyRut(rut *string) {
tempRut := *rut
verifier := strings.ToUpper(tempRut[len(tempRut)-1:])
tempRut = tempRut[:len(tempRut)-1]
tempRut = Reverse(tempRut)
tempRut = InsertNth(tempRut, 3, '.')
tempRut = Reverse(tempRut)
tempRut = tempRut + "-" + verifier
*rut = tempRut
}
func IsValidRut(rut string) (bool, error) {
// rut should be 8 or 9 characters
if len(rut) != 8 && len(rut) != 9 {
return false, errors.New("incorrect RUT length")
}
verifier := strings.ToUpper(rut[len(rut)-1:])
tempRut := rut[:len(rut)-1]
_, err := strconv.Atoi(verifier)
if err != nil && verifier != "K" {
return false, errors.New("invalid RUT identifier")
}
generatedVerifier, err := generateVerifier(tempRut)
if err != nil {
return false, err
}
if verifier != generatedVerifier {
return false, errors.New("incorrect RUT verifier")
}
return true, nil
}
func GetRutType(rut string) (RutType, error) {
tempRut := rut[:len(rut)-1]
numericRut, err := strconv.Atoi(tempRut)
if err != nil {
return Run, errors.New("invalid RUN/RUT")
}
if numericRut < 100000000 && numericRut > 50000000 {
return Rut, nil
} else {
return Run, nil
}
}
func generateVerifier(rut string) (string, error) {
if _, err := strconv.Atoi(rut); err != nil {
return "", errors.New("invalid RUT")
}
var multiplier = 2
var sum = 0
var remainder int
var division int
var rutLength = len(rut)
for i := rutLength - 1; i >= 0; i-- {
sum = sum + toInt(rut[i:i+1])*multiplier
multiplier++
if multiplier == 8 {
multiplier = 2
}
}
division = sum / 11
division = division * 11.0
remainder = sum - int(division)
if remainder != 0 {
remainder = 11 - remainder
}
if remainder == 10 {
return "K", nil
} else {
return strconv.Itoa(remainder), nil
}
}

138
backend/utils/rut_test.go Normal file
View File

@@ -0,0 +1,138 @@
package utils
import (
"testing"
)
func TestRutTypeRun(t *testing.T) {
typeTest := Run
want := "RUN"
msg := typeTest.String()
if msg != want {
t.Fatalf(`typeTest.String() = %q, want match for %#q, nil`, msg, want)
}
}
func TestRutTypeRut(t *testing.T) {
typeTest := Rut
want := "RUT"
msg := typeTest.String()
if msg != want {
t.Fatalf(`typeTest.String() = %q, want match for %#q, nil`, msg, want)
}
}
func TestRutTypeUnknown(t *testing.T) {
var typeTest RutType = 2
want := "unknown"
msg := typeTest.String()
if msg != want {
t.Fatalf(`typeTest.String() = %q, want match for %#q, nil`, msg, want)
}
}
func TestCleanRut(t *testing.T) {
want := "8675309K"
msg := "8.675.309-k"
CleanRut(&msg)
if msg != want {
t.Fatalf(`CleanRut(&msg) = %q, want match for %#q, nil`, msg, want)
}
}
func TestPrettyRut(t *testing.T) {
want := "8.675.309-K"
msg := "8675309k"
PrettyRut(&msg)
if msg != want {
t.Fatalf(`PrettyRut(&msg) = %q, want match for %#q, nil`, msg, want)
}
}
func TestGenerateVerifier(t *testing.T) {
want := "K"
msg, err := generateVerifier("8675309")
if msg != want {
t.Fatalf(`generateVerifier("8675309") = %q, %v, want match for %#q, nil`, msg, err, want)
}
}
func TestGenerateVerifier2(t *testing.T) {
want := "9"
msg, err := generateVerifier("86753095")
if msg != want {
t.Fatalf(`generateVerifier("86753095") = %q, %v, want match for %q, nil`, msg, err, want)
}
}
func TestGenerateVerifierInvalidString(t *testing.T) {
want := "invalid RUT"
_, err := generateVerifier("8675f309")
if err.Error() != want {
t.Fatalf(`generateVerifier("8675f309") = %q, want %q, nil`, err.Error(), want)
}
}
func TestIsValidRut(t *testing.T) {
want := true
msg, err := IsValidRut("8675309K")
if msg != want {
t.Fatalf(`IsValidRut("8675309K") = false, %v, want match for true, nil`, err)
}
}
func TestIsValidRutInvalid(t *testing.T) {
want := false
msg, err := IsValidRut("86753T99")
if msg != want {
t.Fatalf(`IsValidRut("86753T99") = true, %v, want match for false, nil`, err)
}
}
func TestIsValidRutInvalidIdentifier(t *testing.T) {
want := false
msg, err := IsValidRut("8675309C")
if msg != want {
t.Fatalf(`IsValidRut("8675309C") = true, %v, want match for false, nil`, err)
}
}
func TestIsValidRutIncorrectLength(t *testing.T) {
want := false
msg, err := IsValidRut("123234")
if msg != want {
t.Fatalf(`IsValidRut("123234") = true, %v, want match for false, nil`, err)
}
}
func TestIsValidRutIncorrectIdentifier(t *testing.T) {
want := false
msg, err := IsValidRut("86753096")
if msg != want {
t.Fatalf(`IsValidRut("86753096") = true, %v, want match for false, nil`, err)
}
}
func TestGetRutTypeRun(t *testing.T) {
want := Run
msg, err := GetRutType("8675309K")
if msg != want {
t.Fatalf(`GetRutType("8675309") = %q, %v, want match for %#q, nil`, msg, err, want)
}
}
func TestGetRutTypeRut(t *testing.T) {
want := Rut
msg, err := GetRutType("867530959")
if msg != want {
t.Fatalf(`GetRutType("867530959") = %q, %v, want match for %#q, nil`, msg, err, want)
}
}
func TestGetRutTypeRutInvalid(t *testing.T) {
want := "invalid RUN/RUT"
_, err := GetRutType("8675f309")
if err.Error() != want {
t.Fatalf(`GetRutType("8675f309") = %q, want %q, nil`, err.Error(), want)
}
}

32
backend/utils/utils.go Normal file
View File

@@ -0,0 +1,32 @@
package utils
import (
"bytes"
"strconv"
)
func toInt(toConvert string) int {
converted, _ := strconv.Atoi(toConvert)
return converted
}
func Reverse(s string) string {
r := []rune(s)
for i, j := 0, len(r)-1; i < len(r)/2; i, j = i+1, j-1 {
r[i], r[j] = r[j], r[i]
}
return string(r)
}
func InsertNth(s string, n int, symbol rune) string {
var buffer bytes.Buffer
var n_1 = n - 1
var l_1 = len(s) - 1
for i, rune := range s {
buffer.WriteRune(rune)
if i%n == n_1 && i != l_1 {
buffer.WriteRune(symbol)
}
}
return buffer.String()
}

View File

@@ -0,0 +1,37 @@
package utils
import (
"testing"
)
func TestReverse(t *testing.T) {
want := "8675309"
msg := Reverse("9035768")
if msg != want {
t.Fatalf(`Reverse("9035768") = %q, want match for %#q, nil`, msg, want)
}
}
func TestInsertNth(t *testing.T) {
want := "867.530.9"
msg := InsertNth("8675309", 3, '.')
if msg != want {
t.Fatalf(`InsertNth("8675309") = %q, want match for %#q, nil`, msg, want)
}
}
func TestToInt(t *testing.T) {
want := 123
msg := toInt("123")
if msg != want {
t.Fatalf(`toInt("123") = %q, want match for %#q, nil`, msg, want)
}
}
func TestToIntInvalid(t *testing.T) {
want := 123
msg := toInt("12f3")
if msg == want {
t.Fatalf(`toInt("12f3") = %q, want match for %#q, nil`, msg, want)
}
}