From f0628e46821c38487f540805113bb8d050e9d353 Mon Sep 17 00:00:00 2001 From: didier amyot Date: Sat, 28 Mar 2015 20:53:28 -0700 Subject: [PATCH] major refactoring of the api --- api.go | 144 ------------------ api/alldoormen.go | 21 +++ api/alldoormen_test.go | 57 +++++++ api/api.go | 54 +++++++ api/createdoorman.go | 34 +++++ api/createdoorman_test.go | 57 +++++++ dbconnector.go => api/dbconnector.go | 4 +- .../dbconnector_test.go | 6 +- api/doorman.go | 23 +++ api/doormanstate.go | 29 ++++ publisher.go => api/publisher.go | 5 +- publisher_test.go => api/publisher_test.go | 2 +- api/serverspecification.go | 19 +++ api/updatedoorman.go | 47 ++++++ api/util.go | 39 +++++ apiutil_test.go => api/util_test.go | 2 +- api_test.go | 115 -------------- apiutil.go | 23 --- doormen/doormen.go | 7 +- doormen/doormen_test.go | 5 + server.go | 28 ++-- serverstat/serverstats.go | 39 +++++ serverstat/serverstats_test.go | 28 ++++ simpleapi/simpleapi.go | 52 +++++++ simpleapi/simpleapi_test.go | 137 +++++++++++++++++ static/bower.json | 1 - static/coffee/app.coffee | 7 + .../coffee/controllers/serverspecCtrl.coffee | 7 + static/gulpfile.js | 3 +- static/index.html | 56 ++++--- static/partials/apispec.html | 19 ++- static/partials/doorman.html | 23 ++- static/partials/doormanError.html | 5 + static/partials/doormen.html | 2 +- static/partials/home.html | 6 +- static/partials/newdoorman.html | 7 +- static/partials/serverspec.html | 55 +++++++ static_server.go | 20 --- 38 files changed, 821 insertions(+), 367 deletions(-) delete mode 100644 api.go create mode 100644 api/alldoormen.go create mode 100644 api/alldoormen_test.go create mode 100644 api/api.go create mode 100644 api/createdoorman.go create mode 100644 api/createdoorman_test.go rename dbconnector.go => api/dbconnector.go (98%) rename dbconnector_test.go => api/dbconnector_test.go (86%) create mode 100644 api/doorman.go create mode 100644 api/doormanstate.go rename publisher.go => api/publisher.go (98%) rename publisher_test.go => api/publisher_test.go (82%) create mode 100644 api/serverspecification.go create mode 100644 api/updatedoorman.go create mode 100644 api/util.go rename apiutil_test.go => api/util_test.go (97%) delete mode 100644 api_test.go delete mode 100644 apiutil.go create mode 100644 serverstat/serverstats.go create mode 100644 serverstat/serverstats_test.go create mode 100644 simpleapi/simpleapi.go create mode 100644 simpleapi/simpleapi_test.go create mode 100644 static/coffee/controllers/serverspecCtrl.coffee create mode 100644 static/partials/serverspec.html delete mode 100644 static_server.go diff --git a/api.go b/api.go deleted file mode 100644 index 9373ea1..0000000 --- a/api.go +++ /dev/null @@ -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) -} diff --git a/api/alldoormen.go b/api/alldoormen.go new file mode 100644 index 0000000..eeb9438 --- /dev/null +++ b/api/alldoormen.go @@ -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), "]}") +} diff --git a/api/alldoormen_test.go b/api/alldoormen_test.go new file mode 100644 index 0000000..953c3ec --- /dev/null +++ b/api/alldoormen_test.go @@ -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) + } +} diff --git a/api/api.go b/api/api.go new file mode 100644 index 0000000..14441a3 --- /dev/null +++ b/api/api.go @@ -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 +} diff --git a/api/createdoorman.go b/api/createdoorman.go new file mode 100644 index 0000000..9d37912 --- /dev/null +++ b/api/createdoorman.go @@ -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) +} diff --git a/api/createdoorman_test.go b/api/createdoorman_test.go new file mode 100644 index 0000000..fc491e4 --- /dev/null +++ b/api/createdoorman_test.go @@ -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) + } +} diff --git a/dbconnector.go b/api/dbconnector.go similarity index 98% rename from dbconnector.go rename to api/dbconnector.go index 1a9794d..fc6c656 100644 --- a/dbconnector.go +++ b/api/dbconnector.go @@ -1,13 +1,13 @@ -package main +package api import ( "errors" "log" + "github.com/didiercrunch/doormanserver/doormen" "github.com/didiercrunch/doormanserver/inmemoryconnector" "github.com/didiercrunch/doormanserver/mongoconnector" "github.com/didiercrunch/doormanserver/shared" - "github.com/didiercrunch/doormanserver/doormen" "gopkg.in/mgo.v2/bson" ) diff --git a/dbconnector_test.go b/api/dbconnector_test.go similarity index 86% rename from dbconnector_test.go rename to api/dbconnector_test.go index 4fac5aa..849c6ea 100644 --- a/dbconnector_test.go +++ b/api/dbconnector_test.go @@ -1,13 +1,13 @@ -package main +package api import ( "testing" - "github.com/didiercrunch/doormanserver/inmemoryconnector" "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") conn = inmemoryconnector.NewInMemoryDBConnector() diff --git a/api/doorman.go b/api/doorman.go new file mode 100644 index 0000000..26e9eca --- /dev/null +++ b/api/doorman.go @@ -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) + } +} diff --git a/api/doormanstate.go b/api/doormanstate.go new file mode 100644 index 0000000..38dd78c --- /dev/null +++ b/api/doormanstate.go @@ -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) + } +} diff --git a/publisher.go b/api/publisher.go similarity index 98% rename from publisher.go rename to api/publisher.go index a3bd64e..d4bc35d 100644 --- a/publisher.go +++ b/api/publisher.go @@ -1,4 +1,4 @@ -package main +package api import ( "log" @@ -37,7 +37,6 @@ func initPublisher(params *shared.Params) { if params.UseNSQ() { initNSQPublisher() log.Println("using nsq as message queue") - } else if params.UseNanomsg() { initNanoMsgPublisher() log.Println("using nanomsg as message queue") @@ -45,8 +44,8 @@ func initPublisher(params *shared.Params) { publisher = &logpublisher.LogPublisher{log.Println} log.Println("using stdout as message queue") } - } + func init() { initPublisher(shared.GetParams()) } diff --git a/publisher_test.go b/api/publisher_test.go similarity index 82% rename from publisher_test.go rename to api/publisher_test.go index 08107a0..156da73 100644 --- a/publisher_test.go +++ b/api/publisher_test.go @@ -1,4 +1,4 @@ -package main +package api import "testing" diff --git a/api/serverspecification.go b/api/serverspecification.go new file mode 100644 index 0000000..d6dd19d --- /dev/null +++ b/api/serverspecification.go @@ -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) + } +} diff --git a/api/updatedoorman.go b/api/updatedoorman.go new file mode 100644 index 0000000..abb4154 --- /dev/null +++ b/api/updatedoorman.go @@ -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) + +} diff --git a/api/util.go b/api/util.go new file mode 100644 index 0000000..7295664 --- /dev/null +++ b/api/util.go @@ -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()) +} diff --git a/apiutil_test.go b/api/util_test.go similarity index 97% rename from apiutil_test.go rename to api/util_test.go index 720bef5..5dfa85a 100644 --- a/apiutil_test.go +++ b/api/util_test.go @@ -1,4 +1,4 @@ -package main +package api import ( "errors" diff --git a/api_test.go b/api_test.go deleted file mode 100644 index 7774b3b..0000000 --- a/api_test.go +++ /dev/null @@ -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() - } - -} diff --git a/apiutil.go b/apiutil.go deleted file mode 100644 index 7f269d9..0000000 --- a/apiutil.go +++ /dev/null @@ -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()) -} diff --git a/doormen/doormen.go b/doormen/doormen.go index be92d9f..c02f182 100644 --- a/doormen/doormen.go +++ b/doormen/doormen.go @@ -31,8 +31,8 @@ func (wId *DoormanId) AsJson() string { } type DoormanDefinition struct { - Id bson.ObjectId `json:"id,omitempty" bson:"_id"` - Name string `json:"name" bson:"name"` + Id bson.ObjectId `json:"id,omitempty" bson:"_id"` + Name string `json:"name" bson:"name"` Values []*DoormanValue `json:"values" bson:"values"` } @@ -71,13 +71,14 @@ func (dmd *DoormanDefinition) ValidateDoormanValueNames() error { func (dmd *DoormanDefinition) ValidateDoormanValueProbabilities() error { var sum float64 = 0 for _, w := range dmd.Values { + if p := w.Probability; p < 0-epsilon || p > 1+epsilon { return errors.New(fmt.Sprintf("doorman value %v is out of range", p)) } else { 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 nil diff --git a/doormen/doormen_test.go b/doormen/doormen_test.go index 68be1b5..20e2581 100644 --- a/doormen/doormen_test.go +++ b/doormen/doormen_test.go @@ -57,6 +57,11 @@ func TestValidate(t *testing.T) { if err := dmd.Validate(); err.Error() != "the sum of the probability must be 1.0" { 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) { diff --git a/server.go b/server.go index c4cae82..abd6b7f 100644 --- a/server.go +++ b/server.go @@ -2,32 +2,36 @@ package main import ( "fmt" + "github.com/didiercrunch/doormanserver/api" + "github.com/didiercrunch/doormanserver/shared" "log" "net/http" + "path" "strconv" - - "github.com/didiercrunch/doormanserver/shared" - "github.com/gorilla/mux" ) -func createMuxRouter() http.Handler { - r := mux.NewRouter() - serveApi(r.PathPrefix("/api").Subrouter()) - serveStatic(r.PathPrefix("/").Subrouter()) - return r -} +const STATIC_DIR_PATH = "static/" func GetAddressToServe() string { params := shared.GetParams() 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() { - r := createMuxRouter() - http.Handle("/", r) + mux := http.NewServeMux() + mux.Handle("/api/", api.Create()) + mux.HandleFunc("/", serveStatic2) + address := GetAddressToServe() 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) } } diff --git a/serverstat/serverstats.go b/serverstat/serverstats.go new file mode 100644 index 0000000..44fd606 --- /dev/null +++ b/serverstat/serverstats.go @@ -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 +} diff --git a/serverstat/serverstats_test.go b/serverstat/serverstats_test.go new file mode 100644 index 0000000..4f71fd4 --- /dev/null +++ b/serverstat/serverstats_test.go @@ -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) + } +} diff --git a/simpleapi/simpleapi.go b/simpleapi/simpleapi.go new file mode 100644 index 0000000..81bd79e --- /dev/null +++ b/simpleapi/simpleapi.go @@ -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) +} diff --git a/simpleapi/simpleapi_test.go b/simpleapi/simpleapi_test.go new file mode 100644 index 0000000..dac6eca --- /dev/null +++ b/simpleapi/simpleapi_test.go @@ -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) + } +} diff --git a/static/bower.json b/static/bower.json index 2f706a4..e52b01e 100644 --- a/static/bower.json +++ b/static/bower.json @@ -18,7 +18,6 @@ "angular-mocks": "~1.2.1", "requirejs": "~2.1.9", "requirejs-text": "~2.0.10", - "foundation": "zurb/bower-foundation", "html5-boilerplate": "~4.3.0", "angularAMD": "~0.2.1", "angular-foundation": "~0.5.1" diff --git a/static/coffee/app.coffee b/static/coffee/app.coffee index cf1a2ea..3a9749b 100644 --- a/static/coffee/app.coffee +++ b/static/coffee/app.coffee @@ -1,3 +1,4 @@ + define [ "angularAMD" "angular-route" @@ -9,6 +10,10 @@ define [ templateUrl: "partials/home.html" controller: "HomeCtrl" controllerUrl: "controllers/homeCtrl" + )).when("/serverspec", angularAMD.route( + templateUrl: "partials/serverspec.html" + controller: "ServerspecCtrl" + controllerUrl: "controllers/serverspecCtrl" )).when("/apispec", angularAMD.route( templateUrl: "partials/apispec.html" controller: "ApispecCtrl" @@ -32,4 +37,6 @@ define [ )).otherwise redirectTo: "/" return + serverspec + angularAMD.bootstrap(app) diff --git a/static/coffee/controllers/serverspecCtrl.coffee b/static/coffee/controllers/serverspecCtrl.coffee new file mode 100644 index 0000000..bfe5ccb --- /dev/null +++ b/static/coffee/controllers/serverspecCtrl.coffee @@ -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 diff --git a/static/gulpfile.js b/static/gulpfile.js index 091e32a..25c065b 100644 --- a/static/gulpfile.js +++ b/static/gulpfile.js @@ -5,6 +5,7 @@ var help = require('gulp-task-listing'); var clean = require('gulp-clean'); var sass = require('gulp-sass'); var rjs = require("gulp-rjs"); +var gutil = require('gulp-util'); gulp.task('help', help); @@ -22,7 +23,7 @@ gulp.task('sass', ['bower'], function () { gulp.task('coffee', ['bower'], function() { return gulp.src('coffee/**/*.coffee') - .pipe(coffee({bare: true})) + .pipe(coffee({bare: true}).on('error', gutil.log)) .pipe(gulp.dest('js/')) }); diff --git a/static/index.html b/static/index.html index 0612ffe..63c0108 100644 --- a/static/index.html +++ b/static/index.html @@ -8,37 +8,35 @@ -
-
- - - - - -
-
-
- -
-
+ + + + + + + + + + +
+ diff --git a/static/partials/apispec.html b/static/partials/apispec.html index d842cb2..bc45fcc 100644 --- a/static/partials/apispec.html +++ b/static/partials/apispec.html @@ -1,6 +1,6 @@ -
+
-

Api specification

+

Api specification

@@ -15,6 +15,11 @@ + + + + Get the server specification + @@ -40,6 +45,16 @@ Delete the doorman + + + + Return the state of the doormen as expected by the client + + + + + Return the state of the doormen as expected by the client + diff --git a/static/partials/doorman.html b/static/partials/doorman.html index ad4761a..697a20b 100644 --- a/static/partials/doorman.html +++ b/static/partials/doorman.html @@ -1,6 +1,7 @@ +
-

{{ doorman.name }}

+

{{ doorman.name }}

@@ -19,11 +20,27 @@
+ +
diff --git a/static/partials/doormanError.html b/static/partials/doormanError.html index 57692b2..63a3a81 100644 --- a/static/partials/doormanError.html +++ b/static/partials/doormanError.html @@ -1,3 +1,8 @@ +
+
+

error page

+
+
diff --git a/static/partials/doormen.html b/static/partials/doormen.html index 9b2e00c..c469787 100644 --- a/static/partials/doormen.html +++ b/static/partials/doormen.html @@ -1,6 +1,6 @@
-

doormen

+

doormen

diff --git a/static/partials/home.html b/static/partials/home.html index 3ac67ad..6ef8d77 100644 --- a/static/partials/home.html +++ b/static/partials/home.html @@ -1,4 +1,8 @@ -

Welcome to the world best a/b testing library

+
+
+

Welcome to the world best a/b testing library

+
+
diff --git a/static/partials/newdoorman.html b/static/partials/newdoorman.html index 7023efa..ef6fc85 100644 --- a/static/partials/newdoorman.html +++ b/static/partials/newdoorman.html @@ -1,5 +1,8 @@ - -

New Doorman

+
+
+

new doorman

+
+
diff --git a/static/partials/serverspec.html b/static/partials/serverspec.html new file mode 100644 index 0000000..38149ba --- /dev/null +++ b/static/partials/serverspec.html @@ -0,0 +1,55 @@ +
+
+

server specification

+
+
+ +
+
+

general

+ + + + + + + + + + + + + + + + + + + + + +
valuekey
hostname{{server.hostname}}
port{{server.port}}
go version{{server.go_version}}
+
+
+ + +
+
+

nano msg

+ + + + + + + + + + + + + + +
valuekey
url{{server.nano_msg.url}}
+
+
diff --git a/static_server.go b/static_server.go deleted file mode 100644 index 143dcc1..0000000 --- a/static_server.go +++ /dev/null @@ -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) -}