major refactoring of the api

This commit is contained in:
didier amyot committed 2015-03-28 20:53:28 -07:00
1 parent d4f7dc57fc
commit f0628e4682
38 files changed
+808 -354

No files matched your search

-144
View File
@@ -1,144 +0,0 @@
package main
import (
"fmt"
"net/http"
"encoding/json"
"github.com/didiercrunch/doormanserver/doormen"
"github.com/gorilla/mux"
"gopkg.in/mgo.v2/bson"
)
func getDoormenIdsAsJson(id *doormen.DoormanId) string {
return fmt.Sprintf(`{"name": "%v", "id": "%v", "url": "/api/doormen/%v"}`, id.Name, id.Id.Hex(), id.Id.Hex())
}
func CreateDoorman(w http.ResponseWriter, request *http.Request) {
wdef := new(doormen.DoormanDefinition)
defer request.Body.Close()
decoder := json.NewDecoder(request.Body)
if err := decoder.Decode(wdef); err != nil {
w.WriteHeader(400)
fmt.Fprintln(w, "cannot create doorman definition \n", err)
return
}
wdef.Id = bson.NewObjectId()
if err := wdef.Validate(); err != nil {
w.WriteHeader(400)
fmt.Fprintln(w, "New doorman is invalid \n", err)
return
}
if err := CreateDoormanInDatabase(wdef); err != nil {
w.WriteHeader(500)
fmt.Fprintln(w, "Server error \n", err)
return
}
go publisher.Emit(wdef.Id, wdef)
w.Header().Set("location", "/api/doormen/"+wdef.Id.Hex())
w.WriteHeader(201)
}
func GetAllDoormen(w http.ResponseWriter, request *http.Request) {
fmt.Fprint(w, `{"doormen" :[`)
doormenIds := conn.GetAllDoormen()
buff := <-doormenIds
if buff == nil {
fmt.Fprint(w, "]}")
return
}
for doormanId := range doormenIds {
fmt.Fprint(w, getDoormenIdsAsJson(buff)+`,`)
buff = doormanId
}
fmt.Fprint(w, getDoormenIdsAsJson(buff), "]}")
}
func getDoormanIdFromRequest(w http.ResponseWriter, request *http.Request) bson.ObjectId {
id := mux.Vars(request)["id"]
if !bson.IsObjectIdHex(id) {
return ""
} else {
return bson.ObjectIdHex(id)
}
}
func GetDoorman(w http.ResponseWriter, r *http.Request) {
id := getDoormanIdFromRequest(w, r)
if id == "" {
Write404Error(w)
return
}
doorman, err := conn.GetDoorman(id)
if err != nil {
Write500Error(w, err)
} else if doorman == nil {
Write404Error(w)
} else {
encoder := json.NewEncoder(w)
encoder.Encode(doorman)
}
}
func UpdateDoorman(w http.ResponseWriter, request *http.Request) {
id := getDoormanIdFromRequest(w, request)
if id == "" || !conn.ExistsId(id) {
Write404Error(w)
return
}
wdef := new(doormen.DoormanDefinition)
defer request.Body.Close()
decoder := json.NewDecoder(request.Body)
if err := decoder.Decode(wdef); err != nil {
Write400Error(w, err)
return
}
wdef.Id = id
if err := wdef.Validate(); err != nil {
Write400Error(w, err)
return
}
originalDoorman, err := conn.GetDoorman(id)
if err != nil {
Write500Error(w, err)
return
}
if err := originalDoorman.CanBeUpdatedBy(wdef); err != nil {
Write400Error(w, err)
return
}
if err := UpdateDoormanInDatabase(wdef); err != nil {
Write500Error(w, err)
return
}
go publisher.Emit(wdef.Id, wdef)
w.WriteHeader(200)
}
type Api struct {
GetAllDoormen http.HandlerFunc
CreateDoorman http.HandlerFunc
GetDoorman http.HandlerFunc
UpdateDoorman http.HandlerFunc
}
func (a *Api) ServeApi(router *mux.Router) {
router.Path("/doormen").Methods("GET").HandlerFunc(a.GetAllDoormen)
router.Path("/doormen").Methods("POST").HandlerFunc(a.CreateDoorman)
router.Path("/doormen/{id}").Methods("GET").HandlerFunc(a.GetDoorman)
router.Path("/doormen/{id}").Methods("PUT").HandlerFunc(a.UpdateDoorman)
}
func serveApi(router *mux.Router) {
api := new(Api)
api.CreateDoorman = CreateDoorman
api.GetAllDoormen = GetAllDoormen
api.GetDoorman = GetDoorman
api.UpdateDoorman = UpdateDoorman
api.ServeApi(router)
}
+21
View File
@@ -0,0 +1,21 @@
package api
import (
"fmt"
"net/http"
)
func GetAllDoormen(w http.ResponseWriter, request *http.Request) {
fmt.Fprint(w, `{"doormen" :[`)
doormenIds := conn.GetAllDoormen()
buff := <-doormenIds
if buff == nil {
fmt.Fprint(w, "]}")
return
}
for doormanId := range doormenIds {
fmt.Fprint(w, getDoormenIdsAsJson(buff)+`,`)
buff = doormanId
}
fmt.Fprint(w, getDoormenIdsAsJson(buff), "]}")
}
+57
View File
@@ -0,0 +1,57 @@
package api
import (
"encoding/json"
"net/http"
"net/http/httptest"
"testing"
"github.com/didiercrunch/doormanserver/doormen"
"github.com/didiercrunch/doormanserver/inmemoryconnector"
)
func init() {
conn = inmemoryconnector.NewInMemoryDBConnector()
}
func TestGetAllDoormen(t *testing.T) {
conn = inmemoryconnector.NewInMemoryDBConnector()
conn.Save(doormen.QuickNewDoormanDefinition("foo", 0.25, 0.75))
conn.Save(doormen.QuickNewDoormanDefinition("bar", 0.5, 0.5))
req, err := http.NewRequest("GET", "http://bigtits.com/api/doormen", nil)
if err != nil {
t.Error(err)
return
}
w := httptest.NewRecorder()
GetAllDoormen(w, req)
d := json.NewDecoder(w.Body)
m := make(map[string][]*doormen.DoormanDefinition)
if err := d.Decode(&m); err != nil {
t.Error(err)
return
}
if len(m["doormen"]) != 2 {
t.Error()
}
}
func TestGetAllDoormenEmpty(t *testing.T) {
conn = inmemoryconnector.NewInMemoryDBConnector()
req, err := http.NewRequest("GET", "http://bigtits.com/api/doormen", nil)
if err != nil {
t.Error(err)
return
}
w := httptest.NewRecorder()
GetAllDoormen(w, req)
if s := w.Body.String(); s != `{"doormen" :[]}` {
t.Error(s)
}
}
+54
View File
@@ -0,0 +1,54 @@
package api
import (
"github.com/didiercrunch/doormanserver/simpleapi"
"net/http"
)
func Create() http.Handler {
serverSpecification := &simpleapi.Endpoint{
"/server",
"Get the server specification.",
"GET",
GetServerSpecification,
}
createDoorman := &simpleapi.Endpoint{
"/doormen",
"Create a new doorman.",
"POST",
CreateDoorman,
}
allDoorman := &simpleapi.Endpoint{
"/doormen",
"Get the list of all the public doormen.",
"GET",
GetAllDoormen,
}
doorman := &simpleapi.Endpoint{
"/doormen/{id}",
"Get the doorman.",
"GET",
GetDoorman,
}
updateDoorman := &simpleapi.Endpoint{
"/doormen/{id}",
"Update the doorman with new probabilities.",
"PUT",
UpdateDoorman,
}
api := simpleapi.New(
"/api",
serverSpecification,
createDoorman,
allDoorman,
doorman,
updateDoorman,
)
return api
}
+34
View File
@@ -0,0 +1,34 @@
package api
import (
"encoding/json"
"fmt"
"github.com/didiercrunch/doormanserver/doormen"
"gopkg.in/mgo.v2/bson"
"net/http"
)
func CreateDoorman(w http.ResponseWriter, request *http.Request) {
wdef := new(doormen.DoormanDefinition)
defer request.Body.Close()
decoder := json.NewDecoder(request.Body)
if err := decoder.Decode(wdef); err != nil {
w.WriteHeader(400)
fmt.Fprintln(w, "cannot create doorman definition \n", err)
return
}
wdef.Id = bson.NewObjectId()
if err := wdef.Validate(); err != nil {
w.WriteHeader(400)
fmt.Fprintln(w, "New doorman is invalid \n", err)
return
}
if err := CreateDoormanInDatabase(wdef); err != nil {
w.WriteHeader(500)
fmt.Fprintln(w, "Server error \n", err)
return
}
go publisher.Emit(wdef.Id, wdef)
w.Header().Set("location", "/api/doormen/"+wdef.Id.Hex())
w.WriteHeader(201)
}
+57
View File
@@ -0,0 +1,57 @@
package api
import (
"bytes"
"github.com/didiercrunch/doormanserver/inmemoryconnector"
"gopkg.in/mgo.v2/bson"
"net/http"
"net/http/httptest"
"path"
"testing"
)
func GetIdFromLocation(p string) bson.ObjectId {
return bson.ObjectIdHex(path.Base(p))
}
func TestCreateDoorman(t *testing.T) {
conn = inmemoryconnector.NewInMemoryDBConnector()
payload := `{"name":"name","values":[{"name":"red","probability":0.2},{"name":"blue","probability":0.8}]}`
req, err := http.NewRequest(
"POST",
"http://bigtits.com/api/doormen",
bytes.NewBufferString(payload),
)
if err != nil {
t.Error(err)
return
}
w := httptest.NewRecorder()
CreateDoorman(w, req)
if w.Code != 201 {
t.Error("bad status code", w.Code)
} else if location := w.Header().Get("location"); location == "" {
t.Error("missing location header", location)
} else if dor, _ := conn.GetDoorman(GetIdFromLocation(location)); dor == nil {
t.Error("dorman not created")
}
}
func TestCreateDoormanWithBadInput(t *testing.T) {
conn = inmemoryconnector.NewInMemoryDBConnector()
payload := `{"name":"name","values":[{"name":"red","probability":0.2},{"name":"blue","probability":0.2}]}`
req, err := http.NewRequest(
"POST",
"http://bigtits.com/api/doormen",
bytes.NewBufferString(payload),
)
if err != nil {
t.Error(err)
return
}
w := httptest.NewRecorder()
CreateDoorman(w, req)
if w.Code != 400 {
t.Error("bad status code", w.Code)
}
}
+2 -2
View File
@@ -1,13 +1,13 @@
package main package api
import ( import (
"errors" "errors"
"log" "log"
"github.com/didiercrunch/doormanserver/doormen"
"github.com/didiercrunch/doormanserver/inmemoryconnector" "github.com/didiercrunch/doormanserver/inmemoryconnector"
"github.com/didiercrunch/doormanserver/mongoconnector" "github.com/didiercrunch/doormanserver/mongoconnector"
"github.com/didiercrunch/doormanserver/shared" "github.com/didiercrunch/doormanserver/shared"
"github.com/didiercrunch/doormanserver/doormen"
"gopkg.in/mgo.v2/bson" "gopkg.in/mgo.v2/bson"
) )
@@ -1,13 +1,13 @@
package main package api
import ( import (
"testing" "testing"
"github.com/didiercrunch/doormanserver/inmemoryconnector"
"github.com/didiercrunch/doormanserver/doormen" "github.com/didiercrunch/doormanserver/doormen"
"github.com/didiercrunch/doormanserver/inmemoryconnector"
) )
func TestCreateDoorman(t *testing.T) { func TestCreateDoormanInDatabase(t *testing.T) {
wab := doormen.NewDoormanDefinition("some_name") wab := doormen.NewDoormanDefinition("some_name")
conn = inmemoryconnector.NewInMemoryDBConnector() conn = inmemoryconnector.NewInMemoryDBConnector()
+23
View File
@@ -0,0 +1,23 @@
package api
import (
"encoding/json"
"net/http"
)
func GetDoorman(w http.ResponseWriter, r *http.Request) {
id := getDoormanIdFromRequest(w, r)
if id == "" {
Write404Error(w)
return
}
doorman, err := conn.GetDoorman(id)
if err != nil {
Write500Error(w, err)
} else if doorman == nil {
Write404Error(w)
} else {
encoder := json.NewEncoder(w)
encoder.Encode(doorman)
}
}
+29
View File
@@ -0,0 +1,29 @@
package api
import (
"mime"
"net/http"
)
func GetDormanState(w http.ResponseWriter, request *http.Request) {
id := getDoormanIdFromRequest(w, request)
if id == "" {
Write404Error(w)
return
}
doorman, err := conn.GetDoorman(id)
if err != nil {
Write500Error(w, err)
return
} else if doorman == nil {
Write404Error(w)
return
}
if payload, err := doorman.AsDoormanUpdatePayload(); err != nil {
Write500Error(w, err)
} else {
w.Header().Add("Content-Type", mime.TypeByExtension(".json"))
w.Write(payload)
}
}
+2 -3
View File
@@ -1,4 +1,4 @@
package main package api
import ( import (
"log" "log"
@@ -37,7 +37,6 @@ func initPublisher(params *shared.Params) {
if params.UseNSQ() { if params.UseNSQ() {
initNSQPublisher() initNSQPublisher()
log.Println("using nsq as message queue") log.Println("using nsq as message queue")
} else if params.UseNanomsg() { } else if params.UseNanomsg() {
initNanoMsgPublisher() initNanoMsgPublisher()
log.Println("using nanomsg as message queue") log.Println("using nanomsg as message queue")
@@ -45,8 +44,8 @@ func initPublisher(params *shared.Params) {
publisher = &logpublisher.LogPublisher{log.Println} publisher = &logpublisher.LogPublisher{log.Println}
log.Println("using stdout as message queue") log.Println("using stdout as message queue")
} }
} }
func init() { func init() {
initPublisher(shared.GetParams()) initPublisher(shared.GetParams())
} }
+1 -1
View File
@@ -1,4 +1,4 @@
package main package api
import "testing" import "testing"
+19
View File
@@ -0,0 +1,19 @@
package api
import (
"encoding/json"
"github.com/didiercrunch/doormanserver/serverstat"
"github.com/didiercrunch/doormanserver/shared"
"log"
"mime"
"net/http"
)
func GetServerSpecification(w http.ResponseWriter, request *http.Request) {
s := serverstat.Get(shared.GetParams())
w.Header().Add("Content-Type", mime.TypeByExtension(".json"))
encoder := json.NewEncoder(w)
if err := encoder.Encode(s); err != nil {
log.Println(err)
}
}
+47
View File
@@ -0,0 +1,47 @@
package api
import (
"encoding/json"
"github.com/didiercrunch/doormanserver/doormen"
"net/http"
)
func UpdateDoorman(w http.ResponseWriter, request *http.Request) {
id := getDoormanIdFromRequest(w, request)
if id == "" || !conn.ExistsId(id) {
Write404Error(w)
return
}
wdef := new(doormen.DoormanDefinition)
defer request.Body.Close()
decoder := json.NewDecoder(request.Body)
if err := decoder.Decode(wdef); err != nil {
Write400Error(w, err)
return
}
wdef.Id = id
if err := wdef.Validate(); err != nil {
Write400Error(w, err)
return
}
originalDoorman, err := conn.GetDoorman(id)
if err != nil {
Write500Error(w, err)
return
}
if err := originalDoorman.CanBeUpdatedBy(wdef); err != nil {
Write400Error(w, err)
return
}
if err := UpdateDoormanInDatabase(wdef); err != nil {
Write500Error(w, err)
return
}
go publisher.Emit(wdef.Id, wdef)
w.WriteHeader(200)
}
+39
View File
@@ -0,0 +1,39 @@
package api
import (
"fmt"
"github.com/didiercrunch/doormanserver/doormen"
"github.com/gorilla/mux"
"gopkg.in/mgo.v2/bson"
"log"
"net/http"
)
func getDoormenIdsAsJson(id *doormen.DoormanId) string {
return fmt.Sprintf(`{"name": "%v", "id": "%v", "url": "/api/doormen/%v"}`, id.Name, id.Id.Hex(), id.Id.Hex())
}
func getDoormanIdFromRequest(w http.ResponseWriter, request *http.Request) bson.ObjectId {
id := mux.Vars(request)["id"]
if !bson.IsObjectIdHex(id) {
return ""
} else {
return bson.ObjectIdHex(id)
}
}
func Write500Error(w http.ResponseWriter, err error) {
w.WriteHeader(500)
fmt.Fprint(w, "server error")
log.Println(err)
}
func Write404Error(w http.ResponseWriter) {
w.WriteHeader(404)
fmt.Fprint(w, "url not found")
}
func Write400Error(w http.ResponseWriter, err error) {
w.WriteHeader(400)
fmt.Fprint(w, err.Error())
}
+1 -1
View File
@@ -1,4 +1,4 @@
package main package api
import ( import (
"errors" "errors"
-115
View File
@@ -1,115 +0,0 @@
package main
import (
"bytes"
"encoding/json"
"fmt"
"io/ioutil"
"net/http"
"net/http/httptest"
"testing"
"github.com/didiercrunch/doormanserver/inmemoryconnector"
"github.com/didiercrunch/doormanserver/doormen"
"github.com/gorilla/mux"
)
func init() {
conn = inmemoryconnector.NewInMemoryDBConnector()
}
func OkHandler(w http.ResponseWriter, request *http.Request) {
fmt.Fprintf(w, "ok")
}
func assertBodyIs(t *testing.T, res *http.Response, expt string) {
if body, err := ioutil.ReadAll(res.Body); err != nil {
t.Error(err)
} else if s := string(body); s != expt {
t.Error("expected ", expt, " but received ", s)
}
}
func TestPOSTToDoormen(t *testing.T) {
api := &Api{CreateDoorman: OkHandler}
r := mux.NewRouter()
api.ServeApi(r)
ts := httptest.NewServer(r)
defer ts.Close()
res, err := http.Post(ts.URL+"/doormen", "", bytes.NewBufferString("boby"))
if err != nil {
t.Error(err)
return
}
assertBodyIs(t, res, "ok")
}
func TestGETToDoormen(t *testing.T) {
api := &Api{GetAllDoormen: OkHandler}
r := mux.NewRouter()
api.ServeApi(r)
ts := httptest.NewServer(r)
defer ts.Close()
res, err := http.Get(ts.URL + "/doormen")
if err != nil {
t.Error(err)
return
}
assertBodyIs(t, res, "ok")
}
func TestGetAllDoormenEmpty(t *testing.T) {
conn = inmemoryconnector.NewInMemoryDBConnector()
api := &Api{GetAllDoormen: GetAllDoormen}
r := mux.NewRouter()
api.ServeApi(r)
ts := httptest.NewServer(r)
defer ts.Close()
res, err := http.Get(ts.URL + "/doormen")
if err != nil {
t.Error(err)
return
}
if body, err := ioutil.ReadAll(res.Body); err != nil {
t.Error(err)
} else if s := string(body); s != `{"doormen" :[]}` {
t.Error(s)
}
}
func TestGetAllDoormen(t *testing.T) {
conn = inmemoryconnector.NewInMemoryDBConnector()
conn.Save(doormen.QuickNewDoormanDefinition("foo", 0.25, 0.75))
conn.Save(doormen.QuickNewDoormanDefinition("bar", 0.5, 0.5))
api := &Api{GetAllDoormen: GetAllDoormen}
r := mux.NewRouter()
api.ServeApi(r)
ts := httptest.NewServer(r)
defer ts.Close()
res, err := http.Get(ts.URL + "/doormen")
if err != nil {
t.Error(err)
return
}
d := json.NewDecoder(res.Body)
m := make(map[string][]*doormen.DoormanDefinition)
if err := d.Decode(&m); err != nil {
t.Error(err)
return
}
if len(m["doormen"]) != 2 {
t.Error()
}
}
-23
View File
@@ -1,23 +0,0 @@
package main
import (
"fmt"
"log"
"net/http"
)
func Write500Error(w http.ResponseWriter, err error) {
w.WriteHeader(500)
fmt.Fprint(w, "server error")
log.Println(err)
}
func Write404Error(w http.ResponseWriter) {
w.WriteHeader(404)
fmt.Fprint(w, "url not found")
}
func Write400Error(w http.ResponseWriter, err error) {
w.WriteHeader(400)
fmt.Fprint(w, err.Error())
}
+2 -1
View File
@@ -71,13 +71,14 @@ func (dmd *DoormanDefinition) ValidateDoormanValueNames() error {
func (dmd *DoormanDefinition) ValidateDoormanValueProbabilities() error { func (dmd *DoormanDefinition) ValidateDoormanValueProbabilities() error {
var sum float64 = 0 var sum float64 = 0
for _, w := range dmd.Values { for _, w := range dmd.Values {
if p := w.Probability; p < 0-epsilon || p > 1+epsilon { if p := w.Probability; p < 0-epsilon || p > 1+epsilon {
return errors.New(fmt.Sprintf("doorman value %v is out of range", p)) return errors.New(fmt.Sprintf("doorman value %v is out of range", p))
} else { } else {
sum += p sum += p
} }
} }
if sum < 0-epsilon || sum > 1+epsilon { if sum < 1-epsilon || sum > 1+epsilon {
return errors.New("the sum of the probability must be 1.0") return errors.New("the sum of the probability must be 1.0")
} }
return nil return nil
+5
View File
@@ -57,6 +57,11 @@ func TestValidate(t *testing.T) {
if err := dmd.Validate(); err.Error() != "the sum of the probability must be 1.0" { if err := dmd.Validate(); err.Error() != "the sum of the probability must be 1.0" {
t.Error(err) t.Error(err)
} }
dmd = &DoormanDefinition{Name: "foo", Values: []*DoormanValue{&DoormanValue{"foo", 0.2}, &DoormanValue{"bar", 0.2}}}
if err := dmd.Validate(); err.Error() != "the sum of the probability must be 1.0" {
t.Error(err)
}
} }
func TestAsJson(t *testing.T) { func TestAsJson(t *testing.T) {
+16 -12
View File
@@ -2,32 +2,36 @@ package main
import ( import (
"fmt" "fmt"
"github.com/didiercrunch/doormanserver/api"
"github.com/didiercrunch/doormanserver/shared"
"log" "log"
"net/http" "net/http"
"path"
"strconv" "strconv"
"github.com/didiercrunch/doormanserver/shared"
"github.com/gorilla/mux"
) )
func createMuxRouter() http.Handler { const STATIC_DIR_PATH = "static/"
r := mux.NewRouter()
serveApi(r.PathPrefix("/api").Subrouter())
serveStatic(r.PathPrefix("/").Subrouter())
return r
}
func GetAddressToServe() string { func GetAddressToServe() string {
params := shared.GetParams() params := shared.GetParams()
return params.Host + ":" + strconv.Itoa(params.Port) return params.Host + ":" + strconv.Itoa(params.Port)
} }
func serveStatic2(w http.ResponseWriter, request *http.Request) {
filepath := request.URL.Path
w.Header().Set("Cache-Control", "public, max-age=43200")
fmt.Println(STATIC_DIR_PATH, filepath, path.Join(STATIC_DIR_PATH, filepath))
http.ServeFile(w, request, path.Join(STATIC_DIR_PATH, filepath))
}
func main() { func main() {
r := createMuxRouter() mux := http.NewServeMux()
http.Handle("/", r) mux.Handle("/api/", api.Create())
mux.HandleFunc("/", serveStatic2)
address := GetAddressToServe() address := GetAddressToServe()
fmt.Println("server running at", address) fmt.Println("server running at", address)
if err := http.ListenAndServe(address, createMuxRouter()); err != nil { if err := http.ListenAndServe(address, mux); err != nil {
log.Fatal(err) log.Fatal(err)
} }
} }
+39
View File
@@ -0,0 +1,39 @@
package serverstat
import (
"runtime"
"strings"
"github.com/didiercrunch/doormanserver/shared"
)
type m map[string]interface{}
var GO_VERSION = runtime.Version()
type ServerStat struct {
Hostname string `json:"hostname"`
Port int `json:"port"`
Go string `json:"go_version"`
MessageQueue string `json:"message_queue"`
NSQ m `json:"nsq,omitempty"`
Nanomsg m `json:"nano_msg,omitempty"`
}
func Get(p *shared.Params) *ServerStat {
r := new(ServerStat)
r.Hostname = p.Host
r.Port = p.Port
r.Go = GO_VERSION
r.MessageQueue = strings.ToLower(p.MessageQueue)
if p.UseNSQ() {
r.NSQ = m{"ns_lookup_url": p.NSQ.NSQLookupdUrl}
}
if p.UseNanomsg() {
r.Nanomsg = m{"url": p.NanoMsg.NanoMsgUrl}
}
return r
}
+28
View File
@@ -0,0 +1,28 @@
package serverstat
import (
"encoding/json"
"reflect"
"testing"
)
import "github.com/didiercrunch/doormanserver/shared"
func deserializeServerStats(data []byte) *ServerStat {
ret := new(ServerStat)
if err := json.Unmarshal(data, ret); err != nil {
panic("cannot deserialize data")
}
return ret
}
func ServerStatsEqual(a, b *ServerStat) bool {
return reflect.DeepEqual(a, b)
}
func TestGetTrivial(t *testing.T) {
p := new(shared.Params)
expected := &ServerStat{Go: GO_VERSION}
if s := Get(p); !ServerStatsEqual(s, expected) {
t.Error(s)
}
}
+52
View File
@@ -0,0 +1,52 @@
package simpleapi
import (
"github.com/gorilla/mux"
"mime"
"net/http"
"path"
)
func JSONHendler(handler http.HandlerFunc) http.HandlerFunc {
return func(w http.ResponseWriter, request *http.Request) {
w.Header().Add("Content-Type", mime.TypeByExtension(".json"))
handler(w, request)
}
}
type Endpoint struct {
Pattern string
Description string
Method string
HandlerFunc http.HandlerFunc
}
type SimpleApi struct {
apiEndpoints []*Endpoint
router *mux.Router
prefix string
}
func New(prefix string, apiEndpoints ...*Endpoint) *SimpleApi {
api := &SimpleApi{apiEndpoints, nil, prefix}
api.initRouter()
return api
}
func (api *SimpleApi) initRouter() {
api.router = mux.NewRouter()
for _, apiEndpoint := range api.apiEndpoints {
api.router.Path(path.Join(api.prefix, apiEndpoint.Pattern)).
Methods(apiEndpoint.Method).
HandlerFunc(JSONHendler(apiEndpoint.HandlerFunc))
}
}
func (api *SimpleApi) Set404Handler(handler http.HandlerFunc) {
api.router.NotFoundHandler = http.HandlerFunc(handler)
}
func (api *SimpleApi) ServeHTTP(w http.ResponseWriter, request *http.Request) {
api.router.ServeHTTP(w, request)
}
+137
View File
@@ -0,0 +1,137 @@
package simpleapi
import (
"bytes"
"encoding/json"
"io/ioutil"
"net/http"
"net/http/httptest"
"testing"
)
type requestData struct {
Path string
Method string
Response *http.Response
}
func createRequestData(r *http.Request) *requestData {
return &requestData{r.URL.Path, r.Method, nil}
}
func GetHandlerFunc() http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
encoder := json.NewEncoder(w)
encoder.Encode(createRequestData(r))
}
}
func GetMockApiEndpoint(pattern, method string) *Endpoint {
return &Endpoint{Pattern: pattern, Method: method, HandlerFunc: GetHandlerFunc()}
}
func GET(ts *httptest.Server, path string) (*requestData, error) {
res, err := http.Get(ts.URL + path)
if err != nil {
return nil, err
}
body, err := ioutil.ReadAll(res.Body)
if err != nil {
return nil, err
}
res.Body.Close()
ret := new(requestData)
if err := json.Unmarshal(body, ret); err != nil {
return nil, err
}
ret.Response = res
return ret, nil
}
func POST(ts *httptest.Server, path, data string) (*requestData, error) {
res, err := http.Post(ts.URL+path, "", bytes.NewBufferString(data))
if err != nil {
return nil, err
}
body, err := ioutil.ReadAll(res.Body)
if err != nil {
return nil, err
}
res.Body.Close()
ret := new(requestData)
return ret, json.Unmarshal(body, ret)
}
func TestServeGet(t *testing.T) {
api := New("/", GetMockApiEndpoint("/", "GET"))
ts := httptest.NewServer(api)
defer ts.Close()
if rd, err := GET(ts, ""); err != nil {
t.Error(err)
} else if rd.Path != "/" {
t.Error(rd)
}
}
func TestServeGetSpecificPath(t *testing.T) {
api := New("/", GetMockApiEndpoint("/some/path", "GET"))
ts := httptest.NewServer(api)
defer ts.Close()
if rd, err := GET(ts, "/some/path"); err != nil {
t.Error(err)
} else if rd.Path != "/some/path" {
t.Error(rd)
}
}
func TestServePOSTSpecificPath(t *testing.T) {
api := New("/", GetMockApiEndpoint("/some/path", "POST"))
ts := httptest.NewServer(api)
defer ts.Close()
if rd, err := POST(ts, "/some/path", ""); err != nil {
t.Error(err)
} else if rd.Path != "/some/path" || rd.Method != "POST" {
t.Error(rd)
}
}
func Test404(t *testing.T) {
api := New("/")
api.Set404Handler(GetHandlerFunc())
ts := httptest.NewServer(api)
defer ts.Close()
if rd, err := GET(ts, "/some/path"); err != nil {
t.Error(err)
} else if rd.Path != "/some/path" || rd.Method != "GET" {
t.Error(rd)
}
}
func TestHeaders(t *testing.T) {
api := New("/", GetMockApiEndpoint("/", "GET"))
ts := httptest.NewServer(api)
defer ts.Close()
if rd, err := GET(ts, ""); err != nil {
t.Error(err)
} else if rd.Response.Header.Get("Content-Type") != "application/json" {
t.Error(rd)
}
}
func TestServePrefix(t *testing.T) {
api := New("/api", GetMockApiEndpoint("/foo/bar", "GET"))
ts := httptest.NewServer(api)
defer ts.Close()
if rd, err := GET(ts, "/api/foo/bar"); err != nil {
t.Error(err)
} else if rd.Path != "/api/foo/bar" {
t.Error(rd)
}
}
-1
View File
@@ -18,7 +18,6 @@
"angular-mocks": "~1.2.1", "angular-mocks": "~1.2.1",
"requirejs": "~2.1.9", "requirejs": "~2.1.9",
"requirejs-text": "~2.0.10", "requirejs-text": "~2.0.10",
"foundation": "zurb/bower-foundation",
"html5-boilerplate": "~4.3.0", "html5-boilerplate": "~4.3.0",
"angularAMD": "~0.2.1", "angularAMD": "~0.2.1",
"angular-foundation": "~0.5.1" "angular-foundation": "~0.5.1"
+7
View File
@@ -1,3 +1,4 @@
define [ define [
"angularAMD" "angularAMD"
"angular-route" "angular-route"
@@ -9,6 +10,10 @@ define [
templateUrl: "partials/home.html" templateUrl: "partials/home.html"
controller: "HomeCtrl" controller: "HomeCtrl"
controllerUrl: "controllers/homeCtrl" controllerUrl: "controllers/homeCtrl"
)).when("/serverspec", angularAMD.route(
templateUrl: "partials/serverspec.html"
controller: "ServerspecCtrl"
controllerUrl: "controllers/serverspecCtrl"
)).when("/apispec", angularAMD.route( )).when("/apispec", angularAMD.route(
templateUrl: "partials/apispec.html" templateUrl: "partials/apispec.html"
controller: "ApispecCtrl" controller: "ApispecCtrl"
@@ -32,4 +37,6 @@ define [
)).otherwise redirectTo: "/" )).otherwise redirectTo: "/"
return return
serverspec
angularAMD.bootstrap(app) angularAMD.bootstrap(app)
@@ -0,0 +1,7 @@
define ["app"], (app) ->
app.controller "ServerspecCtrl",["$scope", "$http", ($scope, $http) ->
$scope.server = {}
$http.get("/api/server").then (res) -> $scope.server = res.data
return
]
return
+2 -1
View File
@@ -5,6 +5,7 @@ var help = require('gulp-task-listing');
var clean = require('gulp-clean'); var clean = require('gulp-clean');
var sass = require('gulp-sass'); var sass = require('gulp-sass');
var rjs = require("gulp-rjs"); var rjs = require("gulp-rjs");
var gutil = require('gulp-util');
gulp.task('help', help); gulp.task('help', help);
@@ -22,7 +23,7 @@ gulp.task('sass', ['bower'], function () {
gulp.task('coffee', ['bower'], function() { gulp.task('coffee', ['bower'], function() {
return gulp.src('coffee/**/*.coffee') return gulp.src('coffee/**/*.coffee')
.pipe(coffee({bare: true})) .pipe(coffee({bare: true}).on('error', gutil.log))
.pipe(gulp.dest('js/')) .pipe(gulp.dest('js/'))
}); });
+18 -20
View File
@@ -8,37 +8,35 @@
</head> </head>
<body> <body>
<div class="off-canvas-wrap">
<div class="inner-wrap">
<nav class="tab-bar">
<section class="middle tab-bar-section">
<h1 class="title">Doorman</h1>
</section>
<section class="right-small"> <top-bar>
<a class="right-off-canvas-toggle menu-icon" ><span></span></a> <ul class="title-area">
</section> <li class="name">
</nav> <h1><a href="#">Doorman</a></h1>
</li>
<li toggle-top-bar class="menu-icon"><a href="#">Menu</a></li>
</ul>
<aside class="right-off-canvas-menu"> <top-bar-section>
<ul class="off-canvas-list"> <!-- Right Nav Section -->
<li><a></a></li> <ul class="right">
<li has-dropdown>
<a href="#">Dropdown</a>
<ul top-bar-dropdown>
<li><a href="#/doormen">doormen</a></li> <li><a href="#/doormen">doormen</a></li>
<li><a href="#/newdoorman">new doorman</a></li> <li><a href="#/newdoorman">new doorman</a></li>
<li><a href="#/apispec">api specification</a></li> <li><a href="#/apispec">api specification</a></li>
<li><a href="#/serverspec">server specification</a></li> <li><a href="#/serverspec">server specification</a></li>
</ul> </ul>
</aside> </li>
</ul>
<section class="main-section"> </top-bar-section>
<div class="large-12 small-12 columns" ng-view> </top-bar>
</section>
<div class="large-12 small-12 columns" ng-view></div>
<a class="exit-off-canvas"></a>
</div>
</div>
</body> </body>
</html> </html>
+17 -2
View File
@@ -1,6 +1,6 @@
<div class="row wab-apispect"> <div class="row">
<div class="columns large-12"> <div class="columns large-12">
<h2>Api specification</h2> <h1>Api specification</h1>
</div> </div>
</div> </div>
@@ -15,6 +15,11 @@
</tr> </tr>
</thead> </thead>
<tbody> <tbody>
<tr>
<td><wab-api endpoint="/server"></wab-api></td>
<td><wab-method method="get"></wab-method></td>
<td>Get the server specification</td>
</tr>
<tr> <tr>
<td><wab-api endpoint="/doormen"></wab-api></td> <td><wab-api endpoint="/doormen"></wab-api></td>
<td><wab-method method="get"></wab-method></td> <td><wab-method method="get"></wab-method></td>
@@ -40,6 +45,16 @@
<td><wab-method method="delete"></wab-method></td> <td><wab-method method="delete"></wab-method></td>
<td>Delete the doorman</td> <td>Delete the doorman</td>
</tr> </tr>
<tr>
<td><wab-api endpoint="/doormen/:id/status"></wab-api></td>
<td><wab-method method="get"></wab-method></td>
<td>Return the state of the doormen as expected by the client</td>
</tr>
<tr>
<td><wab-api endpoint="/doormen/:id/result?data=<base64>"></wab-api></td>
<td><wab-method method="get"></wab-method></td>
<td>Return the state of the doormen as expected by the client</td>
</tr>
</tbody> </tbody>
</table> </table>
+18 -1
View File
@@ -1,6 +1,7 @@
<div class="row"> <div class="row">
<div class="columns large-12"> <div class="columns large-12">
<h2>{{ doorman.name }} </h2> <h1>{{ doorman.name }} </h1>
</div> </div>
</div> </div>
@@ -19,6 +20,22 @@
</div> </div>
</div> </div>
<!-- <div class="row">
<div class="columns large-12 small-12">
<h3>try it!</h3>
</div>
</div>
<div class="row">
<textarea class="large-offset-1 small-offset-1 large-10 small-10 columns">
</textarea>
</div>
<div class="row">
<div class="large-offset-1 small-offset-1 large-10 small-10 columns">
<a href="#" class="button tiny">try</a>
</div>
</div> -->
<div class="row"> <div class="row">
<div class="large-12 small-12 columns"> <div class="large-12 small-12 columns">
<ul class="button-group"> <ul class="button-group">
+5
View File
@@ -1,3 +1,8 @@
<div class="row">
<div class="columns large-12">
<h1>error page</h1>
</div>
</div>
<div class="row"> <div class="row">
<div class="columns large-12"> <div class="columns large-12">
+1 -1
View File
@@ -1,6 +1,6 @@
<div class="row"> <div class="row">
<div class="columns large-12"> <div class="columns large-12">
<h2>doormen</h2> <h1>doormen</h1>
</div> </div>
</div> </div>
+5 -1
View File
@@ -1,4 +1,8 @@
<h2>Welcome to the world best a/b testing library</h2> <div class="row">
<div class="columns large-12">
<h1>Welcome to the world best a/b testing library</h1>
</div>
</div>
<div class="row"> <div class="row">
<div class="large-12 columns"> <div class="large-12 columns">
+5 -2
View File
@@ -1,5 +1,8 @@
<div class="row">
<h2>New Doorman</h2> <div class="columns large-12">
<h1>new doorman</h1>
</div>
</div>
<form> <form>
<div class="row"> <div class="row">
+55
View File
@@ -0,0 +1,55 @@
<div class="row">
<div class="columns large-12">
<h1>server specification</h1>
</div>
</div>
<div class="row" ng-show="server.message_queue == 'nanomsg'">
<div class="columns large-12">
<h3>general</h3>
<table style="width:100%;">
<thead>
<tr>
<th >value</th>
<th>key</th>
</tr>
</thead>
<tbody>
<tr>
<td>hostname</td>
<td><code>{{server.hostname}}</code></td>
</tr>
<tr>
<td>port</td>
<td><code>{{server.port}}</code></td>
</tr>
<tr>
<td>go version</td>
<td><code>{{server.go_version}}</code></td>
</tr>
</tbody>
</table>
</div>
</div>
<div class="row" ng-show="server.message_queue == 'nanomsg'">
<div class="columns large-12">
<h3>nano msg</h3>
<table style="width:100%;">
<thead>
<tr>
<th >value</th>
<th>key</th>
</tr>
</thead>
<tbody>
<tr>
<td>url</td>
<td><code>{{server.nano_msg.url}}</code></td>
</tr>
</tbody>
</table>
</div>
</div>
-20
View File
@@ -1,20 +0,0 @@
package main
import (
"net/http"
"path"
"github.com/gorilla/mux"
)
const STATIC_DIR_PATH = "static/"
func serveStatic(router *mux.Router) {
handler := func(w http.ResponseWriter, request *http.Request) {
vars := mux.Vars(request)
filepath := "/" + vars["path"]
w.Header().Set("Cache-Control", "public, max-age=43200")
http.ServeFile(w, request, path.Join(STATIC_DIR_PATH, filepath))
}
router.HandleFunc("/{path:.*}", handler)
}