Jump to content
  • Advertisement
Sign in to follow this  
  • entries
    25
  • comments
    26
  • views
    26424

Server and client for SnailLife Go

Sign in to follow this  
Liza Shulyayeva

808 views

Over the last couple of days I restructured SnailLife Go into a server and client. I’m still in the “rough draft” stage, but the top level project structure now looks like this:

gosnaillife
├── client
├── cmd
├── common
├── LICENSE.md
├── README.md
├── server
└── setup

Intent

  • Split application into server and client CLI apps.
  • Create REST API for client-server communication
  • Have a “common” package for structures which will be reused by both server and client
  • Create some rudimentary deployment scripts for both server and client

main.go

I started by creating snaillifesrv/main.go alongside snaillifecli/main.go The cmd directory now looks like this:

cmd
├── snaillifecli
│   └── main.go
└── snaillifesrv
    └── main.go

snaillifecli/main.go

The client main.go runs some simple configuration with viper (right now there is just a server.json config file with the server url to connect to depending on which environment you are running). After running the configuration it waits for user input. Once input is received, it tries to find and run a cobra command by that name.

package main

import "fmt"
import (
	"os"
	"bufio"
	"gitlab.com/drakonka/gosnaillife/client/lib/interfaces/cli"
	"gitlab.com/drakonka/gosnaillife/client/lib/interfaces/cli/commands"
	"runtime"
	"strings"
	"path/filepath"
	"io/ioutil"
	"github.com/spf13/viper"
	"gitlab.com/drakonka/gosnaillife/common/util"
)

func main() {
	fmt.Println("Welcome to SnailLife! The world is your oyster.")
	configureClient()
	if err := commands.RootCmd.Execute(); err != nil {
		fmt.Println(err)
		os.Exit(1)
	}
	waitForInput()
}

func configureClient() {
	projectRoot := getProjectRootPath()
	confPath := projectRoot + "/config"
	envname, err := ioutil.ReadFile(confPath + "/env.conf")
	if err != nil {
		util.HandleErr(err, "")
	}
	envFile := string(envname)
	configPath := confPath + "/" + envFile

	viper.AddConfigPath(configPath)

	// Config client
	viper.SetConfigName("server")
	err = viper.ReadInConfig()
	if err != nil {
		util.HandleErr(err, "")
	}
}

func waitForInput() {
	buf := bufio.NewReader(os.Stdin)
	fmt.Print("> ")
	input, err := buf.ReadBytes('\n')
	if err != nil {
		fmt.Println(err)
	} else {
		cmd, err := cli.TryGetCmd(string(input))
		if err != nil {
			fmt.Println(err)
		} else {
			err := cmd.Execute()
			if err != nil {
				fmt.Println("ERROR: " + err.Error())
			}
		}
	}
	waitForInput()
}

func getProjectRootPath() string {
	_, b, _, _ := runtime.Caller(0)
	folders := strings.Split(b, "/")
	folders = folders[:len(folders)-2]
	path := strings.Join(folders, "/")
	basepath := filepath.Dir(path) + "/client"
	return basepath
}

snaillifesrv/main.go

When launching snaillifesrv, a subcommand is expected immediately. Right now the only supported subcommand is serve, which will start the server.

package main

import "fmt"
import (
	"gitlab.com/drakonka/gosnaillife/server/lib/infrastructure/env"
	"os"
	"runtime"
	"path/filepath"
	"strings"
	"gitlab.com/drakonka/gosnaillife/server/lib/infrastructure"
	"gitlab.com/drakonka/gosnaillife/common"
	"gitlab.com/drakonka/gosnaillife/server/lib/interfaces/cli/commands"
)

var App env.Application

func main() {
	setProjectRootPath()
	confPath := env.ProjectRoot + "/config"
	App = infrastructure.Init(confPath, common.CLI)
	if err := commands.RootCmd.Execute(); err != nil {
		fmt.Println(err)
		os.Exit(1)
	}
}

func setProjectRootPath() {
	_, b, _, _ := runtime.Caller(0)
	folders := strings.Split(b, "/")
	folders = folders[:len(folders)-2]
	path := strings.Join(folders, "/")
	basepath := filepath.Dir(path) + "/server"
	env.ProjectRoot = basepath
}

Client

So far an extremely barebones implementation, it looks like this:

client
├── config
│   ├── config.go
│   ├── dev
│   │   └── server.json
│   └── env.conf
└── lib
    └── interfaces
        └── cli
            ├── cli.go
            ├── cmd.go
            └── commands
                ├── register.go
                ├── root.go
                └── test.go

Right now only the register command is implemented. 

Server

The server is where the bulk of the existing packages ended up going:

server
├── config
│   ├── config.go
│   ├── dev
│   │   ├── auth.json
│   │   └── database.json
│   └── env.conf
└── lib
    ├── domain
    │   ├── item
    │   └── snail
    │       ├── snail.go
    │       └── snailrepo.go
    ├── infrastructure
    │   ├── auth
    │   │   ├── authenticator.go
    │   │   ├── auth.go
    │   │   ├── cli
    │   │   │   ├── auth0
    │   │   │   │   ├── auth0.go
    │   │   │   │   └── tests
    │   │   │   │       ├── auth0_test.go
    │   │   │   │       └── config_test.go
    │   │   │   ├── cli.go
    │   │   │   └── cli.so
    │   │   ├── provider.go
    │   │   └── web
    │   ├── databases
    │   │   ├── database.go
    │   │   ├── mysql
    │   │   │   ├── delete.go
    │   │   │   ├── insert.go
    │   │   │   ├── mysql.go
    │   │   │   ├── retrieve.go
    │   │   │   ├── tests
    │   │   │   │   └── mysql_test.go
    │   │   │   └── update.go
    │   │   ├── repo
    │   │   │   ├── repo.go
    │   │   │   ├── tests
    │   │   │   │   ├── repo_test.go
    │   │   │   │   ├── testmodel_test.go
    │   │   │   │   └── testrepo_test.go
    │   │   │   └── util.go
    │   │   └── tests
    │   │       └── testutil.go
    │   ├── env
    │   │   └── env.go
    │   ├── init.go
    │   └── init_test.go
    └── interfaces
        ├── cli
        │   └── commands
        │       ├── root.go
        │       └── serve.go
        └── restapi
            ├── err.go
            ├── handlers
            │   └── user.go
            ├── handlers.go
            ├── logger.go
            ├── restapi.go
            ├── router.go
            └── routes.go

I followed a lot of the advice from this useful post about creating REST APIs in Go. When the user runs the register command on the client, here is what happens on the server. I have added comments to the copy below to help explain:

package handlers


import (
	"encoding/json"
	"fmt"
	"errors"
	"io/ioutil"
	"io"
	"net/http"
	"gitlab.com/drakonka/gosnaillife/common/restapi"
	"gitlab.com/drakonka/gosnaillife/common/util"
	"strings"
	"gitlab.com/drakonka/gosnaillife/server/lib/infrastructure/auth"
	"gitlab.com/drakonka/gosnaillife/server/lib/infrastructure/env"
	http2 "gitlab.com/drakonka/gosnaillife/common/util/http"
)

func CreateUser(w http.ResponseWriter, r *http.Request) {
	fmt.Println("Creating user")
	var user restapi.UserReq
	body, err := ioutil.ReadAll(io.LimitReader(r.Body, 1048576))
    // The infamous Go error handling - I need a better way.
	if err != nil {
		util.HandleErr(err, "CreateUserErr")
		return
	}
	if err := r.Body.Close(); err != nil {
		util.HandleErr(err, "CreateUserErr")
		return
	}

    // Unmarshal the data we get from the client into UserReq
	if err := json.Unmarshal(body, &user); err != nil {
        // If we were unable to unmarshal, send an error response back to the client
		util.HandleErr(err, "CreateUserErr")
		w.Header().Set("Content-Type", "application/json; charset=UTF-8")
		w.WriteHeader(422) // unprocessable entity
		if err := json.NewEncoder(w).Encode(err); err != nil {
			util.HandleErr(err, "CreateUser")
			return
		}
		return
	}

	fmt.Println("Running registration")
	resBody, err := registerUser(user)
	if err != nil {
		util.HandleErr(err, "CreateUserErr")
	}

    // Start creating a userRes to send back to the client.
	userRes := buildUserResponse(resBody)
	status := http.StatusOK
	if err != nil {
		status = http.StatusInternalServerError
	}
	w.Header().Set("Content-Type", "application/json; charset=UTF-8")
	w.WriteHeader(status)
	if err := json.NewEncoder(w).Encode(userRes); err != nil {
		util.HandleErr(err, "CreateUserErr")
		return
	}
}

func registerUser(user restapi.UserReq) (resBody []byte, err error) {
    // Find an Auth0 provider (that is all we'll support for now)
	var auth0 auth.Provider
	auth0 = env.App.Authenticator.FindProvider("Auth0")
	if auth0 != nil {
		resBody, err = auth0.Register(user.Username, user.Password)
	} else {
		err = errors.New("Auth0 provider not found")
	}
	return resBody, err
}

func buildUserResponse(resBody []byte) (*restapi.UserRes) {
	res := restapi.UserRes{}
    // Find any keys we may find relevant from the Auth0 response body
	m, _ := util.FindInJson(resBody, []string {"_id", "statusCode", "name", "description", "error"})
	httpErr := buildHttpErr(m)
	if id, ok := m["_id"]; ok {
		res.Id = fmt.Sprintf("%v", id)
	}
	res.HttpErr = httpErr
	return &res
}

func buildHttpErr(m map[string]interface{}) (httpErr http2.HttpErr) {
    // The Auth0 response body *sometimes* contains errors in statusCode/name/description format and *sometimes* just contains a single "error" json key
	if sc, ok := m["statusCode"]; ok {
		codeStr := fmt.Sprintf("%v", sc)
		if strings.HasPrefix(codeStr,"4") || strings.HasPrefix(codeStr, "5") {
			scf := sc.(float64)
			httpErr.StatusCode = int(scf)
			httpErr.Name = fmt.Sprintf("%v", m["name"])
			httpErr.Desc = fmt.Sprintf("%v", m["description"])
		}
	} else if error, ok := m["error"]; ok {
		httpErr.StatusCode = 500
		httpErr.Name = "Error"
		httpErr.Desc = fmt.Sprintf("%v", error)
	}
	return httpErr
}

In the end the server sends a UserRes back to the client

package restapi

import (
	"gitlab.com/drakonka/gosnaillife/common/util/http"
)


type UserRes struct {
	HttpErr http.HttpErr `json:"httpErr"`
	Id string `json:"id"`
	Username string `json:"username"`
}

type UserReq struct {
	Username string `json:"username"`
	Password string `json:"password"`
	Connection string `json:"connection"`
}

Deployment

I made a couple of quick scripts to deploy client and server. Note that go-bindata lets you compile your config files into the binary, making for easier distribution (and maybe slightlyimproved security for the secret keys stored in the server config since you don’t have loose configs with credentials sitting around)

Client

#!/bin/sh

echo "Building and installing SnailLife"
go-bindata -o ../../client/config/config.go ../../client/config/...
cd ../../cmd/snaillifecli;
go build
GOBIN=$GOPATH/bin go install

Server

#!/bin/sh

echo "Building and installing SnailLife server"
go-bindata -o ../../server/config/config.go ../../server/config/...
cd ../../server/lib/infrastructure/auth/cli

echo "Building cli.so auth plugin"
go build -buildmode=plugin -o cli.so

echo "Building SnailLifeSrv"
cd ../../../../../cmd/snaillifesrv;
go build
GOBIN=$GOPATH/bin go install

Anyway, as you can see there is a long way to go. Up next I am going to write some tests for the REST API and the cobra commands (which I should really have been doing already).

snaillifecli-register.png

Sign in to follow this  


0 Comments


Recommended Comments

There are no comments to display.

Create an account or sign in to comment

You need to be a member in order to leave a comment

Create an account

Sign up for a new account in our community. It's easy!

Register a new account

Sign in

Already have an account? Sign in here.

Sign In Now
  • Advertisement
  • Advertisement
  • Blog Entries

  • Similar Content

    • By Tristanb4
      I've been making music for about 7 years, I have hundreds of releases on soundcloud and bandcamp. Recently I have stepped up my post production game, pouring long hours into EQ and mixing. Most of my music is in a moody, "foggy" piano style with heavy experimentation through pitch shifting, overdubbing, and live recording. I use a spectrogram EQ to manually shape sounds and scoop out noise in Audacity. I am familiar with many general concepts, applying compression, reverb, high and low pass filters, and pretty much all of the effects in Audacity and many of the pitfalls and lessons of live recording for guitar and piano in my home studio. I am familiar with some other programs like ableton and fruity loops but live recording is my strong suit as opposed to composing music in a DAW. I rely heavily on improvisation, recording large amounts of audio and cutting it down and manipulating it in post as well as doing overdubs. I can put out a project of piano music in a month or so up to what I think is a high / acceptable standard that I personally am happy with.

      I am heavily inspired by Akira Yamaoka's work on the Silent Hill series as well as Angelo Badalamenti. I dream of composing music for games or short films, and feel like I'm ready to take on a project like that, as well as being willing to license my already existing music out which I think would be a perfect fit for the right type of horror game or anything with emotional elements.

      I am currently working on another project that will be released in December or on New Years. I will work for a reasonable amount and have done this out of passion for 5+ years because I love doing it. I feel that I have improved enough now to pursue doing something like this.

      Thank you so much to anyone who even bothers to click any of these links, and thank you for your time!

      Here are my links, and you can also email me directly at tristan.best@gmail.com

      www.soundcloud.com/domonemesis
      https://tristanb.bandcamp.com/
      https://www.facebook.com/TristanBMusic
      https://twitter.com/tbest253


      Other skills: I do all of my own cover art with digital photo editing and subsequently also have about 5 years of experience with that- photography and digital photo manipulation. I can work on marketing materials or art in this way. I play the Piano, Guitar, Synth / String piano etc, and I sing. I have close connections to some other musicians and visual artists. I will be honest if I don't think my music will work for your project or if I'm not sure if I can do something well enough, but I feel comfortable taking on some general audio design as well, including general sound / dialogue recording or noise reduction.
    • By ArcanaDragon
      This game is currently in alpha and all graphics are currently placeholder graphics. Any feedback is appreciated.
      Trailer: https://streamable.com/st2rr
      Game Link: https://arcanadragon.itch.io/hero-land
       
    • By Novakin
      Hi guys
      We have a few positions available for our Viking battle sim. It is a first/third person 3d game set in norway. We are looking for a concept artist and an assistant producer. We have a team of devs already and we are still in pre production but any devs can contact me and I may be able to offer additional positions for the right dev. This is a part time project. Anyone interested must be dedicated and motivated. For more info please contact me
    • By Vyacheslav Leonov
      Hi everyone. I am experienced web dev and I want to try creating games for Android, iOS platforms. I know PHP, JS, some Java.
      For beginning I'd like to create some maze game with many levels, different modes to play. After it I want to create some simple word game or some 2D runner.
      I need to choose right game engine.
      For now I have next options:
      Libgdx Unity Corona SDK So, my questions are:
      What game engine should I choose for beginning? What game engine should I choose for maze game? What game engine should I choose for word game and simple 2D runner?  
      Thanks!
       
       
    • By jb-dev
      This is how menu translate to one another
×

Important Information

By using GameDev.net, you agree to our community Guidelines, Terms of Use, and Privacy Policy.

We are the game development community.

Whether you are an indie, hobbyist, AAA developer, or just trying to learn, GameDev.net is the place for you to learn, share, and connect with the games industry. Learn more About Us or sign up!

Sign me up!