Browse Source

Add new entry POST endpoint and improve errors

master
NGnius 4 years ago
parent
commit
d3dd8e5752
5 changed files with 277 additions and 55 deletions
  1. +127
    -16
      handlers.go
  2. +41
    -13
      json_structs.go
  3. +4
    -1
      main.go
  4. +29
    -15
      sql_service.go
  5. +76
    -10
      sql_structs.go

+ 127
- 16
handlers.go View File

@@ -5,31 +5,36 @@ package main
import (
"encoding/json"
"fmt"
"io/ioutil"
"net/http"
"net/url"
"strconv"
)

func boardHandler(w http.ResponseWriter, r *http.Request) {
w.Header().Add("Content-Type", "application/json")
w.Header().Add("Access-Control-Allow-Origin", "*")
if r.Method != "GET" {
w.WriteHeader(405)
//w.WriteHeader(405)
errorResponse(405, "Non-GET method not allowed at this endpoint", w, r)
return
}
w.Header().Add("content-type", "application/json")
w.Header().Add("Access-Control-Allow-Origin", "*")
args := r.URL.Query()
// check args pre-conditions
if !checkArgExists(args, "board", w) {
errorResponse(400, "Missing required 'boards' URL parameter", w, r)
return
}
board := args.Get("board")
if !checkArgExists(args, "count", w) || !checkArgInt(args, "count", w, 0) {
w.WriteHeader(400)
//w.WriteHeader(400)
errorResponse(400, "Missing required 'count' integer URL parameter", w, r)
return
}
count, _ := strconv.Atoi(args.Get("count"))
if !checkArgExists(args, "start", w) || !checkArgInt(args, "start", w, 0) {
w.WriteHeader(400)
//w.WriteHeader(400)
errorResponse(400, "Missing required 'start' integer URL parameter", w, r)
return
}
start, _ := strconv.Atoi(args.Get("start"))
@@ -39,42 +44,148 @@ func boardHandler(w http.ResponseWriter, r *http.Request) {
//b, ok := boards[board]
if err != nil {
fmt.Println(err)
w.WriteHeader(404)
//w.WriteHeader(404)
errorResponse(404, "Board could not be retrieved: "+err.Error(), w, r)
return
}
bEntries, loadErr := b.SomeEntries(int64(start), int64(start+count))
if loadErr != nil {
fmt.Println(loadErr)
w.WriteHeader(404)
//w.WriteHeader(404)
errorResponse(404, "Board entries could not be retrieved: "+loadErr.Error(), w, r)
return
}
for _, entry := range bEntries {
item, _ := entry.JsonObject()
item, entryErr := entry.JsonObject()
if entryErr != nil {
fmt.Println(entryErr)
}
result.Items = append(result.Items, &item)
}
result.Query = fmt.Sprintf("load board[%s] from %d to %d", board, start, count+start)
result.Query = fmt.Sprintf("load board[name: %s] from %d to %d", board, start, count+start)
result.Complete()
data, err := json.Marshal(result)
if err != nil {
//w.WriteHeader(500)
errorResponse(500, "Unable to convert result into JSON: "+err.Error(), w, r)
return
}
w.Write(data)
}

func playerHandler(w http.ResponseWriter, r *http.Request) {
w.Header().Add("Content-Type", "application/json")
w.Header().Add("Access-Control-Allow-Origin", "*")
if r.Method != "GET" {
//w.WriteHeader(405)
errorResponse(405, "Non-GET method not allowed at this endpoint", w, r)
return
}
args := r.URL.Query()
// check args
if !checkArgExists(args, "id", w) || !checkArgInt(args, "id", w, 1) {
errorResponse(400, "Missing required 'id' integer URL parameter", w, r)
return
}
id, _ := strconv.Atoi(args.Get("id"))
entry_count := 0
if checkArgExists(args, "entries", w) && checkArgInt(args, "entries", w, 0) {
entry_count, _ = strconv.Atoi(args.Get("entries"))
}
// retrieve player
result := NewResult("", r.URL.String())
player := &Player{ID: int64(id)}
loadErr := player.Load()
if loadErr != nil {
fmt.Println(loadErr)
//w.WriteHeader(404)
errorResponse(404, "Player could not be retrieved: "+loadErr.Error(), w, r)
return
}
tempJsonObj, _ := player.JsonObject()
pJsonObj := tempJsonObj.(PlayerJSON)
if entry_count > 0 {
entries, loadErr := player.SomeEntries(int64(entry_count))
if loadErr == nil {
for _, e := range entries {
eJsonObj, entryErr := e.JsonObject()
if entryErr != nil {
fmt.Println(entryErr)
}
pJsonObj.Entries = append(pJsonObj.Entries, eJsonObj.(EntryJSON))
}
}
}
result.Items = []interface{}{pJsonObj}
result.Query = fmt.Sprintf("load player[id: %d]", player.ID)
result.Complete()
data, err := json.Marshal(result)
if err != nil {
w.WriteHeader(500)
//w.WriteHeader(500)
errorResponse(500, "Unable convert result to JSON: "+err.Error(), w, r)
return
}
w.Write(data)
}

func newEntryHandler(w http.ResponseWriter, r *http.Request) {
w.Header().Add("Content-Type", "application/json")
w.Header().Add("Access-Control-Allow-Origin", "*")
if r.Method != "POST" {
//w.WriteHeader(405)
errorResponse(405, "Non-POST method not allowed at this endpoint", w, r)
return
}
data, readErr := ioutil.ReadAll(r.Body)
if readErr != nil {
fmt.Println(readErr)
//w.WriteHeader(500)
errorResponse(500, "Unable to read HTTP request body: "+readErr.Error(), w, r)
return
}
newEntry, jsonErr := UnmarshalNewEntryJSON(data)
if jsonErr != nil {
//w.WriteHeader(400)
errorResponse(400, "Unable to convert request to JSON: "+jsonErr.Error(), w, r)
return
}
sqlErr := newEntrySql(newEntry.Score, newEntry.PlayerID, newEntry.BoardID)
if sqlErr != nil {
fmt.Println(sqlErr)
//w.WriteHeader(500)
errorResponse(500, "Entry could not be created: "+sqlErr.Error(), w, r)
return
}
//w.WriteHeader(204)
errorResponse(200, "New entry created", w, r)
}

func exampleHandler(w http.ResponseWriter, r *http.Request) {
// useless function please ignore
}

// utility functions

func checkArgExists(values url.Values, key string, w http.ResponseWriter) (ok bool) {
ok = values.Get(key) != ""
if !ok {
w.WriteHeader(400)
}
return
}

func checkArgInt(values url.Values, key string, w http.ResponseWriter, min int) (ok bool) {
intVal, err := strconv.Atoi(values.Get(key))
ok = err == nil && intVal >= min
if !ok {
w.WriteHeader(400)
}
return
}

func errorResponse(statusCode int, reason string, w http.ResponseWriter, r *http.Request) {
w.WriteHeader(statusCode)
query := "error"
if statusCode == 200 {
query = "success"
}
errorRes := NewResult(query, r.URL.String())
errorRes.Items = append(errorRes.Items, ErrorJSON{Reason: reason, StatusCode: statusCode})
errorRes.StatusCode = statusCode
data, _ := json.Marshal(errorRes)
w.Write(data)
}

+ 41
- 13
json_structs.go View File

@@ -3,27 +3,47 @@
package main

import (
"encoding/json"
"strconv"
"time"
)

// Board a leaderboard
// BoardJSON a leaderboard
type BoardJSON struct {
ID int64
Entries []EntryJSON
Name string
ID int64
Entries []EntryJSON
Name string
Description string
}

// Entry an entry in a leaderboard
// EntryJSON an entry in a leaderboard
type EntryJSON struct {
ID int64
Rank int64
Score int64
PlayerName string
PlayerURL string
PlayerID int64
BoardID int64
}

// NewEntryJSON a new entry to be saved to a leaderboard
type NewEntryJSON struct {
Score int64
PlayerID int64
BoardID int64
}

// Player a player
func UnmarshalNewEntryJSON(data []byte) (NewEntryJSON, error) {
var neJson NewEntryJSON
jsonErr := json.Unmarshal(data, &neJson)
if jsonErr != nil {
return neJson, jsonErr
}
return neJson, nil
}

// PlayerJSON a player
type PlayerJSON struct {
ID int64
Name string
@@ -32,17 +52,24 @@ type PlayerJSON struct {

// URL get the player's URL
func (p *PlayerJSON) URL() string {
return "/player/" + strconv.Itoa(int(p.ID))
return "/player?id=" + strconv.Itoa(int(p.ID))
}

// ErrorJSON a query error response
type ErrorJSON struct {
Reason string
StatusCode int
}

// Result a query result
type Result struct {
Items []interface{}
Elapsed int64
Count int
Query string
URL string
Start time.Time
StatusCode int
Items []interface{}
Elapsed int64 // query time (ms)
Count int
Query string
URL string
Start time.Time
}

// NewResult build a query struct
@@ -50,6 +77,7 @@ func NewResult(q string, url string) (r Result) {
r.Start = time.Now()
r.Query = q
r.URL = url
r.StatusCode = 200
return
}



+ 4
- 1
main.go View File

@@ -28,6 +28,9 @@ func init() {
initArgs()
serverMux := http.NewServeMux()
serverMux.HandleFunc("/load", boardHandler)
serverMux.HandleFunc("/board", boardHandler)
serverMux.HandleFunc("/player", playerHandler)
serverMux.HandleFunc("/record", newEntryHandler)
handler = serverMux
}

@@ -39,7 +42,7 @@ func main() {
os.Exit(1)
}
// handle interrupt (terminate) signal
signalChan := make(chan os.Signal, 1)
signalChan := make(chan os.Signal)
signal.Notify(signalChan, os.Interrupt)
go func() {
s := <-signalChan


+ 29
- 15
sql_service.go View File

@@ -5,6 +5,7 @@ package main
import (
"database/sql"
"fmt"
"time"

_ "github.com/mattn/go-sqlite3"
)
@@ -62,6 +63,18 @@ func playerByName(name string) (*Player, error) {
return p, db.QueryRow("SELECT * FROM Players WHERE name=? LIMTI 1", name).Scan(p.Intake()...)
}

func newEntrySql(score, player, board int64) error {
tx, _ := db.Begin()
stmt, _ := tx.Prepare("INSERT INTO Entries(score, player, board, time) VALUES (?, ?, ?, ?)")
_, err := stmt.Exec(score, player, board, time.Now().Unix())
if err != nil {
tx.Rollback()
return err
}
tx.Commit()
return nil
}

// internal operations
func sqlBuildTables() {
transaction, txErr := db.Begin()
@@ -73,8 +86,8 @@ func sqlBuildTables() {
//transaction.Exec("INSERT INTO Test (Sometext, Somenumber) VALUES (?,?);", "Hello sqlite", 123)
// build real tables
transaction.Exec("CREATE TABLE IF NOT EXISTS Players (id INTEGER PRIMARY KEY, name TEXT NOT NULL);")
transaction.Exec("CREATE TABLE IF NOT EXISTS Boards (id INTEGER PRIMARY KEY, name TEXT NOT NULL, description TEXT);")
transaction.Exec("CREATE TABLE IF NOT EXISTS Entries (id INTEGER PRIMARY KEY, rank INTEGER, score INTEGER NOT NULL, player INTEGER NOT NULL, board INTEGER NOT NULL, FOREIGN KEY(player) REFERENCES Players(id), FOREIGN KEY(board) REFERENCES Boards(id));")
transaction.Exec("CREATE TABLE IF NOT EXISTS Boards (id INTEGER PRIMARY KEY, name TEXT NOT NULL, description TEXT NOT NULL);")
transaction.Exec("CREATE TABLE IF NOT EXISTS Entries (id INTEGER PRIMARY KEY, rank INTEGER NOT NULL DEFAULT -1, score INTEGER NOT NULL, player INTEGER NOT NULL, board INTEGER NOT NULL, time INTEGER NOT NULL, FOREIGN KEY(player) REFERENCES Players(id), FOREIGN KEY(board) REFERENCES Boards(id));")
transaction.Commit()
}

@@ -111,20 +124,21 @@ func sqlPopulateTables() {
fmt.Printf("Error creating player %d: %s\n", p.ID, err)
}
}
now := time.Now().Unix()
entries := []*Entry{
&Entry{ID: 1, Rank: 1, Score: 1000, Player: players[0].ID, Board: boards[0].ID},
&Entry{ID: 2, Rank: 2, Score: 900, Player: players[1].ID, Board: boards[0].ID},
&Entry{ID: 3, Rank: 3, Score: 400, Player: players[2].ID, Board: boards[0].ID},
&Entry{ID: 4, Rank: 4, Score: 350, Player: players[3].ID, Board: boards[0].ID},
&Entry{ID: 5, Rank: 5, Score: 350, Player: players[4].ID, Board: boards[0].ID},
&Entry{ID: 6, Rank: 6, Score: 250, Player: players[5].ID, Board: boards[0].ID},
&Entry{ID: 7, Rank: 7, Score: 200, Player: players[6].ID, Board: boards[0].ID},
&Entry{ID: 8, Rank: 8, Score: 175, Player: players[7].ID, Board: boards[0].ID},
&Entry{ID: 9, Rank: 9, Score: 150, Player: players[8].ID, Board: boards[0].ID},
&Entry{ID: 10, Rank: 10, Score: 140, Player: players[9].ID, Board: boards[0].ID},
&Entry{ID: 11, Rank: 11, Score: 10, Player: players[10].ID, Board: boards[0].ID},
&Entry{ID: 12, Rank: 12, Score: 60, Player: players[11].ID, Board: boards[0].ID},
&Entry{ID: 13, Rank: 13, Score: 13, Player: players[12].ID, Board: boards[0].ID},
&Entry{ID: 1, Rank: 1, Score: 1000, Player: players[0].ID, Board: boards[0].ID, Time: now},
&Entry{ID: 2, Rank: 2, Score: 900, Player: players[1].ID, Board: boards[0].ID, Time: now},
&Entry{ID: 3, Rank: 3, Score: 400, Player: players[2].ID, Board: boards[0].ID, Time: now},
&Entry{ID: 4, Rank: 4, Score: 350, Player: players[3].ID, Board: boards[0].ID, Time: now},
&Entry{ID: 5, Rank: 5, Score: 350, Player: players[4].ID, Board: boards[0].ID, Time: now},
&Entry{ID: 6, Rank: 6, Score: 250, Player: players[5].ID, Board: boards[0].ID, Time: now},
&Entry{ID: 7, Rank: 7, Score: 200, Player: players[6].ID, Board: boards[0].ID, Time: now},
&Entry{ID: 8, Rank: 8, Score: 175, Player: players[7].ID, Board: boards[0].ID, Time: now},
&Entry{ID: 9, Rank: 9, Score: 150, Player: players[8].ID, Board: boards[0].ID, Time: now},
&Entry{ID: 10, Rank: 10, Score: 140, Player: players[9].ID, Board: boards[0].ID, Time: now},
&Entry{ID: 11, Rank: 11, Score: 10, Player: players[10].ID, Board: boards[0].ID, Time: now},
&Entry{ID: 12, Rank: 12, Score: 60, Player: players[11].ID, Board: boards[0].ID, Time: now},
&Entry{ID: 13, Rank: 13, Score: 13, Player: players[12].ID, Board: boards[0].ID, Time: now},
}
for _, e := range entries {
err := e.Commit()


+ 76
- 10
sql_structs.go View File

@@ -5,6 +5,9 @@ package main
import (
"encoding/json"
"strconv"

// test
"fmt"
)

type Jsonable interface {
@@ -27,7 +30,10 @@ type Board struct {

func LoadBoard(id int64) *Board {
b := &Board{ID: id}
b.Load()
loadErr := b.Load()
if loadErr != nil {
return nil
}
return b
}

@@ -76,6 +82,31 @@ func (b *Board) SomeEntries(start, end int64) ([]*Entry, error) {
return entries, nil
}

// implementation of Jsonable
func (b *Board) Json() ([]byte, error) {
var data []byte
jsonObj, err := b.JsonObject()
if err != nil {
return data, err
}
return json.Marshal(jsonObj)
}

func (b *Board) JsonPretty() ([]byte, error) {
var data []byte
jsonObj, err := b.JsonObject()
if err != nil {
return data, err
}
return json.MarshalIndent(jsonObj, "", " ")
}

func (b *Board) JsonObject() (interface{}, error) {
jsonObj := BoardJSON{ID: b.ID, Name: b.Name, Description: b.Description}
return jsonObj, nil
}

// implementation of Rower
func (b *Board) Intake() []interface{} {
return []interface{}{&b.ID, &b.Name, &b.Description}
}
@@ -91,7 +122,10 @@ type Player struct {

func LoadPlayer(id int64) *Player {
p := &Player{ID: id}
p.Load()
loadErr := p.Load()
if loadErr != nil {
return nil
}
return p
}

@@ -127,21 +161,49 @@ func (p *Player) Entries() ([]*Entry, error) {

func (p *Player) SomeEntries(limit int64) ([]*Entry, error) {
var entries []*Entry
rows, err := db.Query("SELECT * FROM Entries WHERE player=? ORDER BY rank ASC LIMIT ?;", p.ID, limit)
rows, err := db.Query("SELECT * FROM Entries WHERE player=? ORDER BY time DESC LIMIT ?;", p.ID, limit)
if err != nil {
return entries, err
}
count := 0
for rows.Next() {
entries = append(entries, &Entry{})
rows.Scan(entries[count].Intake()...)
//rows.Scan(entries[count].Intake()...)
scanErr := rows.Scan(&entries[count].ID, &entries[count].Rank, &entries[count].Score, &entries[count].Player, &entries[count].Board, &entries[count].Time)
if scanErr != nil {
fmt.Println(scanErr)
}
count++
}
return entries, nil
}

func (p *Player) Url() string {
return "/player/" + strconv.Itoa(int(p.ID))
return "/player?id=" + strconv.Itoa(int(p.ID))
}

// implementation of Jsonable
func (p *Player) Json() ([]byte, error) {
var data []byte
jsonObj, err := p.JsonObject()
if err != nil {
return data, err
}
return json.Marshal(jsonObj)
}

func (p *Player) JsonPretty() ([]byte, error) {
var data []byte
jsonObj, err := p.JsonObject()
if err != nil {
return data, err
}
return json.MarshalIndent(jsonObj, "", " ")
}

func (p *Player) JsonObject() (interface{}, error) {
jsonObj := PlayerJSON{ID: p.ID, Name: p.Name}
return jsonObj, nil
}

// implementation of Rower
@@ -159,11 +221,15 @@ type Entry struct {
Score int64
Player int64
Board int64
Time int64 // Created time (seconds since Unix epoch)
}

func LoadEntry(id int64) *Entry {
e := &Entry{ID: id}
e.Load()
loadErr := e.Load()
if loadErr != nil {
return nil
}
return e
}

@@ -173,7 +239,7 @@ func (e *Entry) Load() error {

func (e *Entry) Commit() error {
tx, _ := db.Begin()
statement, _ := tx.Prepare("INSERT OR REPLACE INTO Entries(id, rank, score, player, board) VALUES (?, ?, ?, ?, ?);")
statement, _ := tx.Prepare("INSERT OR REPLACE INTO Entries(id, rank, score, player, board, time) VALUES (?, ?, ?, ?, ?, ?);")
_, err := statement.Exec(e.Output()...)
if err != nil {
tx.Rollback()
@@ -202,7 +268,7 @@ func (e *Entry) JsonPretty() ([]byte, error) {
}

func (e *Entry) JsonObject() (interface{}, error) {
jsonObj := EntryJSON{ID: e.ID, Rank: e.Rank, Score: e.Score}
jsonObj := EntryJSON{ID: e.ID, Rank: e.Rank, Score: e.Score, PlayerID: e.Player, BoardID: e.Board}
ePlayer := &Player{ID: e.Player}
err := ePlayer.Load()
if err != nil {
@@ -215,9 +281,9 @@ func (e *Entry) JsonObject() (interface{}, error) {

// implementation of Rower
func (e *Entry) Intake() []interface{} {
return []interface{}{&e.ID, &e.Rank, &e.Score, &e.Player, &e.Board}
return []interface{}{&e.ID, &e.Rank, &e.Score, &e.Player, &e.Board, &e.Time}
}

func (e *Entry) Output() []interface{} {
return []interface{}{e.ID, e.Rank, e.Score, e.Player, e.Board}
return []interface{}{e.ID, e.Rank, e.Score, e.Player, e.Board, e.Time}
}