diff --git a/.vscode/launch.json b/.vscode/launch.json new file mode 100644 index 0000000..b3d3cce --- /dev/null +++ b/.vscode/launch.json @@ -0,0 +1,18 @@ +{ + // Use IntelliSense to learn about possible attributes. + // Hover to view descriptions of existing attributes. + // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 + "version": "0.2.0", + "configurations": [ + { + "name": "Launch Rove Server", + "type": "go", + "request": "launch", + "mode": "auto", + "program": "${workspaceFolder}/", + "cwd": "${workspaceFolder}", + "env": {}, + "args": [], + } + ] +} \ No newline at end of file diff --git a/Dockerfile b/Dockerfile index 24095ad..2f52a78 100644 --- a/Dockerfile +++ b/Dockerfile @@ -5,6 +5,6 @@ WORKDIR /app COPY . . RUN go mod download -RUN cd cmd/rove-server && go build ./... +RUN go build -o rove-server . -CMD "./cmd/rove-server/rove-server" \ No newline at end of file +CMD "./rove-server" \ No newline at end of file diff --git a/cmd/rove-server/main.go b/main.go similarity index 67% rename from cmd/rove-server/main.go rename to main.go index 22efc1f..2729d78 100644 --- a/cmd/rove-server/main.go +++ b/main.go @@ -13,11 +13,14 @@ import ( var port = flag.Int("port", 8080, "The port to host on") func main() { - server := server.NewServer(*port) + s := server.NewServer( + server.OptionPort(*port), + server.OptionPersistentData()) fmt.Println("Initialising...") - - server.Initialise() + if err := s.Initialise(); err != nil { + panic(err) + } // Set up the close handler c := make(chan os.Signal) @@ -25,10 +28,15 @@ func main() { go func() { <-c fmt.Println("SIGTERM recieved, exiting...") + s.Close() os.Exit(0) }() fmt.Println("Initialised") - server.Run() + s.Run() + + if err := s.Close(); err != nil { + panic(err) + } } diff --git a/pkg/accounts/accounts.go b/pkg/accounts/accounts.go new file mode 100644 index 0000000..efcf976 --- /dev/null +++ b/pkg/accounts/accounts.go @@ -0,0 +1,86 @@ +package accounts + +import ( + "encoding/json" + "fmt" + "io/ioutil" + "os" + + "github.com/google/uuid" +) + +const kDefaultSavePath = "/tmp/accounts.json" + +// Account represents a registered user +type Account struct { + // Name simply describes the account and must be unique + Name string `json:"name"` + + // id represents a unique ID per account and is set one registered + Id uuid.UUID `json:"id"` +} + +// Represents the accountant data to store +type accountantData struct { + Accounts []Account `json:"accounts"` +} + +// Accountant manages a set of accounts +type Accountant struct { + data accountantData +} + +// NewAccountant creates a new accountant +func NewAccountant() *Accountant { + return &Accountant{} +} + +// RegisterAccount adds an account to the set of internal accounts +func (a *Accountant) RegisterAccount(acc Account) (Account, error) { + + // Set the account ID to a new UUID + acc.Id = uuid.New() + + // Verify this acount isn't already registered + for _, a := range a.data.Accounts { + if a.Name == acc.Name { + return Account{}, fmt.Errorf("Account name already registered") + } else if a.Id == acc.Id { + return Account{}, fmt.Errorf("Account ID already registered") + } + } + + // Simply add the account to the list + a.data.Accounts = append(a.data.Accounts, acc) + + return acc, nil +} + +// Load will load the accountant from data +func (a *Accountant) Load() error { + // Don't load anything if the file doesn't exist + _, err := os.Stat(kDefaultSavePath) + if os.IsNotExist(err) { + fmt.Printf("File %s didn't exist, loading with fresh accounts data\n", kDefaultSavePath) + return nil + } + + if b, err := ioutil.ReadFile(kDefaultSavePath); err != nil { + return err + } else if err := json.Unmarshal(b, &a.data); err != nil { + return err + } + return nil +} + +// Save will save the accountant data out +func (a *Accountant) Save() error { + if b, err := json.Marshal(a.data); err != nil { + return err + } else { + if err := ioutil.WriteFile(kDefaultSavePath, b, os.ModePerm); err != nil { + return err + } + } + return nil +} diff --git a/pkg/accounts/accounts_test.go b/pkg/accounts/accounts_test.go new file mode 100644 index 0000000..6d319a2 --- /dev/null +++ b/pkg/accounts/accounts_test.go @@ -0,0 +1,92 @@ +package accounts + +import ( + "testing" +) + +func TestNewAccountant(t *testing.T) { + // Very basic verify here for now + accountant := NewAccountant() + if accountant == nil { + t.Error("Failed to create accountant") + } +} + +func TestAccountant_RegisterAccount(t *testing.T) { + + accountant := NewAccountant() + + // Start by making two accounts + + namea := "one" + a := Account{Name: namea} + acca, err := accountant.RegisterAccount(a) + if err != nil { + t.Error(err) + } else if acca.Name != namea { + t.Errorf("Missmatched account name after register, expected: %s, actual: %s", namea, acca.Name) + } + + nameb := "two" + b := Account{Name: nameb} + accb, err := accountant.RegisterAccount(b) + if err != nil { + t.Error(err) + } else if accb.Name != nameb { + t.Errorf("Missmatched account name after register, expected: %s, actual: %s", nameb, acca.Name) + } + + // Verify our accounts have differing IDs + if acca.Id == accb.Id { + t.Error("Duplicate account IDs fo separate accounts") + } + + // Verify another request gets rejected + _, err = accountant.RegisterAccount(a) + if err == nil { + t.Error("Duplicate account name did not produce error") + } +} + +func TestAccountant_LoadSave(t *testing.T) { + accountant := NewAccountant() + if len(accountant.data.Accounts) != 0 { + t.Error("New accountant created with non-zero account number") + } + + name := "one" + a := Account{Name: name} + a, err := accountant.RegisterAccount(a) + if err != nil { + t.Error(err) + } + + if len(accountant.data.Accounts) != 1 { + t.Error("No new account made") + } else if accountant.data.Accounts[0].Name != name { + t.Error("New account created with wrong name") + } + + // Save out the accountant + if err := accountant.Save(); err != nil { + t.Error(err) + } + + // Re-create the accountant + accountant = NewAccountant() + if len(accountant.data.Accounts) != 0 { + t.Error("New accountant created with non-zero account number") + } + + // Load the old accountant data + if err := accountant.Load(); err != nil { + t.Error(err) + } + + // Verify we have the same account again + if len(accountant.data.Accounts) != 1 { + t.Error("No account after load") + } else if accountant.data.Accounts[0].Name != name { + t.Error("New account created with wrong name") + } +} diff --git a/pkg/server/accounts.go b/pkg/server/accounts.go deleted file mode 100644 index 2f68051..0000000 --- a/pkg/server/accounts.go +++ /dev/null @@ -1,47 +0,0 @@ -package server - -import ( - "fmt" - - "github.com/google/uuid" -) - -// Account represents a registered user -type Account struct { - // Name simply describes the account and must be unique - Name string - - // id represents a unique ID per account and is set one registered - id uuid.UUID -} - -// Accountant manages a set of accounts -type Accountant struct { - accounts []Account -} - -// NewAccountant creates a new accountant -func NewAccountant() *Accountant { - return &Accountant{} -} - -// RegisterAccount adds an account to the set of internal accounts -func (a *Accountant) RegisterAccount(acc Account) (Account, error) { - - // Set the account ID to a new UUID - acc.id = uuid.New() - - // Verify this acount isn't already registered - for _, a := range a.accounts { - if a.Name == acc.Name { - return Account{}, fmt.Errorf("Account name already registered") - } else if a.id == acc.id { - return Account{}, fmt.Errorf("Account ID already registered") - } - } - - // Simply add the account to the list - a.accounts = append(a.accounts, acc) - - return acc, nil -} diff --git a/pkg/server/accounts_test.go b/pkg/server/accounts_test.go deleted file mode 100644 index 8398ba0..0000000 --- a/pkg/server/accounts_test.go +++ /dev/null @@ -1,49 +0,0 @@ -package server - -import ( - "testing" -) - -func TestNewAccountant(t *testing.T) { - // Very basic verify here for now - accountant := NewAccountant() - if accountant == nil { - t.Error("Failed to create accountant") - } -} - -func TestAccountant_RegisterAccount(t *testing.T) { - - accountant := NewAccountant() - - // Start by making two accounts - - namea := "one" - a := Account{Name: namea} - acca, err := accountant.RegisterAccount(a) - if err != nil { - t.Error(err) - } else if acca.Name != namea { - t.Errorf("Missmatched account name after register, expected: %s, actual: %s", namea, acca.Name) - } - - nameb := "two" - b := Account{Name: nameb} - accb, err := accountant.RegisterAccount(b) - if err != nil { - t.Error(err) - } else if accb.Name != nameb { - t.Errorf("Missmatched account name after register, expected: %s, actual: %s", nameb, acca.Name) - } - - // Verify our accounts have differing IDs - if acca.id == accb.id { - t.Error("Duplicate account IDs fo separate accounts") - } - - // Verify another request gets rejected - _, err = accountant.RegisterAccount(a) - if err == nil { - t.Error("Duplicate account name did not produce error") - } -} diff --git a/pkg/server/router.go b/pkg/server/router.go index 5246dd2..bb8fc78 100644 --- a/pkg/server/router.go +++ b/pkg/server/router.go @@ -6,6 +6,7 @@ import ( "net/http" "github.com/gorilla/mux" + "github.com/mdiluz/rove/pkg/accounts" ) // NewRouter sets up the server mux @@ -24,7 +25,7 @@ type StatusResponse struct { // HandleStatus handles HTTP requests to the /status endpoint func (s *Server) HandleStatus(w http.ResponseWriter, r *http.Request) { - fmt.Printf("%s\t%s", r.Method, r.RequestURI) + fmt.Printf("%s\t%s\n", r.Method, r.RequestURI) // Verify we're hit with a get request if r.Method != http.MethodGet { @@ -59,7 +60,12 @@ type RegisterResponse struct { // HandleRegister handles HTTP requests to the /register endpoint func (s *Server) HandleRegister(w http.ResponseWriter, r *http.Request) { - fmt.Printf("%s\t%s", r.Method, r.RequestURI) + fmt.Printf("%s\t%s\n", r.Method, r.RequestURI) + + // Set up the response + var response = RegisterResponse{ + Success: false, + } // Verify we're hit with a get request if r.Method != http.MethodPost { @@ -69,23 +75,26 @@ func (s *Server) HandleRegister(w http.ResponseWriter, r *http.Request) { // Pull out the registration info var data RegisterData - json.NewDecoder(r.Body).Decode(&data) + err := json.NewDecoder(r.Body).Decode(&data) + if err != nil { + fmt.Printf("Failed to decode json: %s", err) - // Register the account with the server - acc := Account{Name: data.Name} - acc, err := s.accountant.RegisterAccount(acc) - - // Set up the response - var response = RegisterResponse{ - Success: false, - } - - // If we didn't fail, respond with the account ID string - if err == nil { - response.Success = true - response.Id = acc.id.String() - } else { response.Error = err.Error() + } else { + // log the data sent + fmt.Printf("\t%v\n", data) + + // Register the account with the server + acc := accounts.Account{Name: data.Name} + acc, err := s.accountant.RegisterAccount(acc) + + // If we didn't fail, respond with the account ID string + if err == nil { + response.Success = true + response.Id = acc.Id.String() + } else { + response.Error = err.Error() + } } // Be a good citizen and set the header for the return diff --git a/pkg/server/router_test.go b/pkg/server/router_test.go index 1983770..6daecbd 100644 --- a/pkg/server/router_test.go +++ b/pkg/server/router_test.go @@ -12,7 +12,7 @@ func TestHandleStatus(t *testing.T) { request, _ := http.NewRequest(http.MethodGet, "/status", nil) response := httptest.NewRecorder() - s := NewServer(8080) + s := NewServer() s.Initialise() s.HandleStatus(response, request) @@ -35,7 +35,7 @@ func TestHandleRegister(t *testing.T) { request, _ := http.NewRequest(http.MethodPost, "/register", bytes.NewReader(b)) response := httptest.NewRecorder() - s := NewServer(8080) + s := NewServer() s.Initialise() s.HandleRegister(response, request) diff --git a/pkg/server/server.go b/pkg/server/server.go index f1024e4..8e581e5 100644 --- a/pkg/server/server.go +++ b/pkg/server/server.go @@ -6,37 +6,84 @@ import ( "net/http" "github.com/gorilla/mux" + "github.com/mdiluz/rove/pkg/accounts" "github.com/mdiluz/rove/pkg/game" ) +const ( + // PersistentData will allow the server to load and save it's state + PersistentData = iota + + // EphemeralData will let the server neither load or save out any of it's data + EphemeralData +) + // Server contains the relevant data to run a game server type Server struct { port int - accountant *Accountant + accountant *accounts.Accountant world *game.World router *mux.Router + + persistence int } -// NewServer sets up a new server -func NewServer(port int) *Server { - return &Server{ - port: port, - accountant: NewAccountant(), - world: game.NewWorld(), +// ServerOption defines a server creation option +type ServerOption func(s *Server) + +// OptionPort sets the server port for hosting +func OptionPort(port int) ServerOption { + return func(s *Server) { + s.port = port } } +// OptionPersistentData sets the server data to be persistent +func OptionPersistentData() ServerOption { + return func(s *Server) { + s.persistence = PersistentData + } +} + +// NewServer sets up a new server +func NewServer(opts ...ServerOption) *Server { + + // Set up the default server + s := &Server{ + port: 8080, + accountant: accounts.NewAccountant(), + world: game.NewWorld(), + persistence: EphemeralData, + } + + // Apply all options + for _, o := range opts { + o(s) + } + + return s +} + // Initialise sets up internal state ready to serve -func (s *Server) Initialise() { +func (s *Server) Initialise() error { // Set up the world s.world = game.NewWorld() fmt.Printf("World created\n\t%+v\n", s.world) + // Load the accounts if requested + if s.persistence == PersistentData { + if err := s.accountant.Load(); err != nil { + return err + } + } + // Create a new router s.SetUpRouter() fmt.Printf("Routes Created\n") + + return nil } // Run executes the server @@ -47,3 +94,15 @@ func (s *Server) Run() { log.Fatal(err) } } + +// Close closes up the server +func (s *Server) Close() error { + + // Save the accounts if requested + if s.persistence == PersistentData { + if err := s.accountant.Save(); err != nil { + return err + } + } + return nil +} diff --git a/script/test.sh b/script/test.sh index 2f8d90b..6fb0aee 100755 --- a/script/test.sh +++ b/script/test.sh @@ -5,7 +5,7 @@ cd .. set -x # Test the build -go build -v ./... +go build -v . # Run unit tests go test -v ./... -cover