@@ -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) | |||
} |
@@ -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 | |||
} | |||
@@ -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 | |||
@@ -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() | |||
@@ -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} | |||
} |