first commit

way too late.
This commit is contained in:
didier amyot committed 2015-02-06 22:03:05 -08:00
commit d4f7dc57fc
53 files changed
+3518

No files matched your search

+30
View File
@@ -0,0 +1,30 @@
# Compiled Object files, Static and Dynamic libs (Shared Objects)
*.o
*.a
*.so
# Folders
_obj
_test
# Architecture specific extensions/prefixes
*.[568vq]
[568vq].out
*.cgo1.go
*.cgo2.c
_cgo_defun.c
_cgo_gotypes.go
_cgo_export.*
_testmain.go
*.exe
*.test
*.prof
*.out
/static/js
/static/css/style.css
/doormanserver
+8
View File
@@ -0,0 +1,8 @@
---
language: go
go:
- 1.3
- 1.4
- tip
+22
View File
@@ -0,0 +1,22 @@
The MIT License (MIT)
Copyright (c) 2014 didier amyot
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
+5
View File
@@ -0,0 +1,5 @@
# Doorman server [![Build Status](https://travis-ci.org/didiercrunch/doormanserver.svg)](https://travis-ci.org/didiercrunch/doormanserver)
This is the server of the doorman project. It is the centralized user interface
where you control the configuration of your A/B tests.
+144
View File
@@ -0,0 +1,144 @@
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)
}
+115
View File
@@ -0,0 +1,115 @@
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
@@ -0,0 +1,23 @@
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())
}
+37
View File
@@ -0,0 +1,37 @@
package main
import (
"errors"
"net/http/httptest"
"testing"
)
func TestWrite404Error(t *testing.T) {
rw := httptest.NewRecorder()
Write404Error(rw)
if rw.Code != 404 {
t.Error()
}
}
func TestWrite500Error(t *testing.T) {
rw := httptest.NewRecorder()
Write500Error(rw, errors.New("blah"))
if rw.Code != 500 {
t.Error()
}
if string(rw.Body.Bytes()) != "server error" {
t.Error()
}
}
func TestWrite400Error(t *testing.T) {
rw := httptest.NewRecorder()
Write400Error(rw, errors.New("blah"))
if rw.Code != 400 {
t.Error()
}
if string(rw.Body.Bytes()) != "blah" {
t.Error()
}
}
+52
View File
@@ -0,0 +1,52 @@
package main
import (
"errors"
"log"
"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"
)
type DBConnector interface {
Save(wab *doormen.DoormanDefinition) error
Update(wab *doormen.DoormanDefinition) error
Exists(wab *doormen.DoormanDefinition) bool
ExistsId(id bson.ObjectId) bool
GetAllDoormen() <-chan *doormen.DoormanId
GetDoorman(id bson.ObjectId) (*doormen.DoormanDefinition, error)
DeleteDoorman(id bson.ObjectId) error
}
var conn DBConnector
func initMongoDbConnector() {
var err error
if conn, err = mongoconnector.NewMongoDBConnector(); err != nil {
panic(err)
}
}
func init() {
if params := shared.GetParams(); params.UseMongoDb() {
initMongoDbConnector()
log.Println("using mongodb as database")
} else {
conn = inmemoryconnector.NewInMemoryDBConnector()
log.Println("using inmemory as database")
}
}
func CreateDoormanInDatabase(wab *doormen.DoormanDefinition) error {
if conn.Exists(wab) {
return errors.New("doormen already exists")
}
return conn.Save(wab)
}
func UpdateDoormanInDatabase(wab *doormen.DoormanDefinition) error {
return conn.Update(wab)
}
+21
View File
@@ -0,0 +1,21 @@
package main
import (
"testing"
"github.com/didiercrunch/doormanserver/inmemoryconnector"
"github.com/didiercrunch/doormanserver/doormen"
)
func TestCreateDoorman(t *testing.T) {
wab := doormen.NewDoormanDefinition("some_name")
conn = inmemoryconnector.NewInMemoryDBConnector()
if err := CreateDoormanInDatabase(wab); err != nil {
t.Error(err)
}
if err := CreateDoormanInDatabase(wab); err == nil {
t.Error("should have an error here")
}
}
+118
View File
@@ -0,0 +1,118 @@
package doormen
import (
"encoding/json"
"errors"
"fmt"
"strconv"
"time"
"github.com/didiercrunch/doorman/shared"
"gopkg.in/mgo.v2/bson"
)
const epsilon float64 = 0.0001
type DoormanValue struct {
Name string `json:"name" bson:"name"`
Probability float64 `json:"probability" bson:"probability"`
}
type DoormanId struct {
Id bson.ObjectId `json:"id,omitempty" bson:"_id"`
Name string `json:"name" bson:"name"`
}
func (wId *DoormanId) AsJson() string {
if wId.Id == "" {
return `{}`
}
return fmt.Sprintf(`{"%v":"%v"}`, wId.Id.Hex(), wId.Name)
}
type DoormanDefinition struct {
Id bson.ObjectId `json:"id,omitempty" bson:"_id"`
Name string `json:"name" bson:"name"`
Values []*DoormanValue `json:"values" bson:"values"`
}
func NewDoormanDefinition(name string) *DoormanDefinition {
return &DoormanDefinition{Id: bson.NewObjectId(), Name: name}
}
// a way to quickly create doormen in tests. do not use in real code. will
// panic if a doorman is wrongly specify
func QuickNewDoormanDefinition(name string, probs ...float64) *DoormanDefinition {
ret := NewDoormanDefinition(name)
for i, prob := range probs {
ret.Values = append(ret.Values, &DoormanValue{"T" + strconv.Itoa(i), prob})
}
if err := ret.Validate(); err != nil {
panic(err)
}
return ret
}
func (dmd *DoormanDefinition) ValidateDoormanValueNames() error {
m := make(map[string]bool)
for _, w := range dmd.Values {
if name := w.Name; name == "" {
return errors.New("doorman value cannot be empty")
} else {
m[name] = true
}
}
if len(m) != len(dmd.Values) {
return errors.New("doorman value names must be unique within a doorman")
}
return nil
}
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 {
return errors.New("the sum of the probability must be 1.0")
}
return nil
}
func (dmd *DoormanDefinition) Validate() (err error) {
switch {
case dmd.Name == "":
err = errors.New("name cannot be empty")
case len(dmd.Values) < 2:
err = errors.New("need at least 2 values")
case dmd.ValidateDoormanValueNames() != nil:
err = dmd.ValidateDoormanValueNames()
case dmd.ValidateDoormanValueProbabilities() != nil:
err = dmd.ValidateDoormanValueProbabilities()
}
return
}
func (dmd *DoormanDefinition) CanBeUpdatedBy(dmd2 *DoormanDefinition) error {
if dmd.Name != dmd2.Name {
return errors.New("cannot change doorman name")
}
if len(dmd.Values) != len(dmd2.Values) {
return errors.New("cannot change the number of cases in doorman")
}
return nil
}
func (dmd *DoormanDefinition) AsDoormanUpdatePayload() ([]byte, error) {
wu := &shared.DoormanUpdater{Id: dmd.Id.Hex()}
wu.Timestamp = time.Now().Unix()
wu.Probabilities = make([]float64, len(dmd.Values))
for i, value := range dmd.Values {
wu.Probabilities[i] = value.Probability
}
return json.Marshal(wu)
}
+124
View File
@@ -0,0 +1,124 @@
package doormen
import (
"encoding/json"
"math"
"testing"
"github.com/didiercrunch/doorman/shared"
"gopkg.in/mgo.v2/bson"
)
func TestNewDoormanDefinition(t *testing.T) {
dmd := NewDoormanDefinition("some_name")
if dmd.Id == "" {
t.Error("need id at all time")
}
if dmd.Name != "some_name" {
t.Error("bad name")
}
}
func TestValidate(t *testing.T) {
dmd := &DoormanDefinition{Name: ""}
if err := dmd.Validate(); err.Error() != "name cannot be empty" {
t.Error(err)
}
dmd = &DoormanDefinition{Name: "foo"}
if err := dmd.Validate(); err.Error() != "need at least 2 values" {
t.Error(err)
}
dmd = &DoormanDefinition{Name: "foo", Values: []*DoormanValue{nil}}
if err := dmd.Validate(); err.Error() != "need at least 2 values" {
t.Error(err)
}
dmd = &DoormanDefinition{Name: "foo", Values: []*DoormanValue{&DoormanValue{"foo", 0}, &DoormanValue{"foo", 0}}}
if err := dmd.Validate(); err.Error() != "doorman value names must be unique within a doorman" {
t.Error(err)
}
dmd = &DoormanDefinition{Name: "foo", Values: []*DoormanValue{&DoormanValue{"foo", 0}, &DoormanValue{"", 0}}}
if err := dmd.Validate(); err.Error() != "doorman value cannot be empty" {
t.Error(err)
}
dmd = &DoormanDefinition{Name: "foo", Values: []*DoormanValue{&DoormanValue{"foo", -0.25}, &DoormanValue{"bar", 0.5}}}
if err := dmd.Validate(); err.Error() != "doorman value -0.25 is out of range" {
t.Error(err)
}
dmd = &DoormanDefinition{Name: "foo", Values: []*DoormanValue{&DoormanValue{"foo", 2.5}, &DoormanValue{"bar", 0.5}}}
if err := dmd.Validate(); err.Error() != "doorman value 2.5 is out of range" {
t.Error(err)
}
dmd = &DoormanDefinition{Name: "foo", Values: []*DoormanValue{&DoormanValue{"foo", 0.75}, &DoormanValue{"bar", 0.5}}}
if err := dmd.Validate(); err.Error() != "the sum of the probability must be 1.0" {
t.Error(err)
}
}
func TestAsJson(t *testing.T) {
id := &DoormanId{}
if `{}` != id.AsJson() {
t.Fail()
}
id.Name = "bob"
if `{}` != id.AsJson() {
t.Fail()
}
id.Id = bson.ObjectIdHex("54af464af6fb5e20c4000003")
if `{"54af464af6fb5e20c4000003":"bob"}` != id.AsJson() {
t.Error(id.AsJson())
}
}
func TestAsDoormanUpdatePayload(t *testing.T) {
wd := &DoormanDefinition{}
wd.Id = bson.NewObjectId()
wd.Values = []*DoormanValue{&DoormanValue{"C", 0.25}, &DoormanValue{"T1", 0.75}}
data, err := wd.AsDoormanUpdatePayload()
if err != nil {
t.Error(err)
return
}
wu := &shared.DoormanUpdater{}
if err := json.Unmarshal(data, wu); err != nil {
t.Error(err)
}
if wd.Id.Hex() != wu.Id {
t.Error("bad id", wu.Id)
}
if len(wu.Probabilities) != 2 {
t.Error("bad probability length")
} else if math.Abs(wu.Probabilities[0]-0.25) > epsilon {
t.Error("bad first probability")
} else if math.Abs(wu.Probabilities[1]-0.75) > epsilon {
t.Error("bad second probability")
} else if wu.Timestamp == 0 {
t.Error("no time specify")
} else if wu.Timestamp < 1421537442 {
t.Error("timestamp looks weird")
}
}
func TestQuickNewDoormanDefinition(t *testing.T) {
dmd := QuickNewDoormanDefinition("foo", 0.75, 0.25)
if dmd.Name != "foo" {
t.Error()
}
if len(dmd.Values) != 2 {
t.Error()
}
if dmd.Values[0].Name != "T0" || dmd.Values[0].Probability != 0.75 {
t.Error()
}
if dmd.Values[1].Name != "T1" || dmd.Values[1].Probability != 0.25 {
t.Error()
}
}
@@ -0,0 +1,106 @@
package inmemoryconnector
import (
"testing"
"github.com/didiercrunch/doormanserver/doormen"
"gopkg.in/mgo.v2/bson"
)
func createMockDoorman(name string) *doormen.DoormanDefinition {
return doormen.QuickNewDoormanDefinition(name, 0.5, 0.5)
}
func TestNewInMemoryDBConnector(t *testing.T) {
c := NewInMemoryDBConnector()
if len(c.memory) != 0 {
t.Error("length at this point should be 0")
}
}
func TestSave(t *testing.T) {
c := NewInMemoryDBConnector()
if err := c.Save(nil); err != nil {
t.Error(err)
}
if len(c.memory) != 1 {
t.Fail()
}
}
func TestDeleteDoorman(t *testing.T) {
c := NewInMemoryDBConnector()
c.Save(createMockDoorman("a"))
c.Save(createMockDoorman("b"))
c.Save(createMockDoorman("c"))
if err := c.DeleteDoorman(c.memory[1].Id); err != nil {
t.Error()
}
if c.memory[0].Name != "a" || c.memory[1].Name != "c" {
t.Error()
}
if err := c.DeleteDoorman(c.memory[1].Id); err != nil {
t.Error()
}
if c.memory[0].Name != "a" {
t.Error()
}
if err := c.DeleteDoorman(c.memory[0].Id); err != nil {
t.Error()
}
if len(c.memory) != 0 {
t.Error()
}
}
func TestGetAllDoormen(t *testing.T) {
c := NewInMemoryDBConnector()
c.Save(createMockDoorman("a"))
c.Save(createMockDoorman("b"))
c.Save(createMockDoorman("c"))
i := 0
expt := []string{"a", "b", "c"}
for w := range c.GetAllDoormen() {
if w.Name != expt[i] {
t.Error(i)
}
i++
}
}
func TestGetDoorman(t *testing.T) {
c := NewInMemoryDBConnector()
wab := createMockDoorman("a")
c.Save(wab)
if w, err := c.GetDoorman(wab.Id); err != nil {
t.Error(err)
} else if w.Name != "a" {
t.Error()
}
if w, err := c.GetDoorman(createMockDoorman("b").Id); err != nil {
t.Error("missing doorman is not an error")
} else if w != nil {
t.Error("should be nil")
}
}
func TestExistsId(t *testing.T) {
c := NewInMemoryDBConnector()
w := createMockDoorman("a")
c.Save(w)
if !c.ExistsId(w.Id) {
t.Error()
}
if c.ExistsId(bson.NewObjectId()) {
t.Error()
}
}
+83
View File
@@ -0,0 +1,83 @@
package inmemoryconnector
import (
"errors"
"github.com/didiercrunch/doormanserver/doormen"
"gopkg.in/mgo.v2/bson"
)
type InMemoryDBConnector struct {
memory []*doormen.DoormanDefinition
}
func NewInMemoryDBConnector() *InMemoryDBConnector {
return &InMemoryDBConnector{make([]*doormen.DoormanDefinition, 0)}
}
func (c *InMemoryDBConnector) Save(dmd *doormen.DoormanDefinition) error {
c.memory = append(c.memory, dmd)
return nil
}
func (c *InMemoryDBConnector) Exists(dmd *doormen.DoormanDefinition) bool {
for _, w := range c.memory {
if w.Name == dmd.Name {
return true
}
}
return false
}
func (c *InMemoryDBConnector) ExistsId(id bson.ObjectId) bool {
for _, w := range c.memory {
if w.Id == id {
return true
}
}
return false
}
func (c *InMemoryDBConnector) GetAllDoormen() <-chan *doormen.DoormanId {
ch := make(chan *doormen.DoormanId)
go func() {
for _, s := range c.memory {
ch <- &doormen.DoormanId{Name: s.Name, Id: s.Id}
}
close(ch)
}()
return ch
}
func (c *InMemoryDBConnector) GetDoorman(id bson.ObjectId) (*doormen.DoormanDefinition, error) {
for _, doorman := range c.memory {
if doorman.Id == id {
return doorman, nil
}
}
return nil, nil
}
func (c *InMemoryDBConnector) DeleteDoorman(id bson.ObjectId) error {
toRemove := -1
for i, doorman := range c.memory {
if doorman.Id == id {
toRemove = i
break
}
}
c.memory = append(c.memory[0:toRemove], c.memory[toRemove+1:len(c.memory)]...)
return nil
}
func (c *InMemoryDBConnector) Update(dmd *doormen.DoormanDefinition) error {
for i, w := range c.memory {
if w.Id == dmd.Id {
c.memory[i] = dmd
return nil
}
}
return errors.New("nothing to update")
}
+26
View File
@@ -0,0 +1,26 @@
package logpublisher
import (
"log"
"github.com/didiercrunch/doormanserver/shared"
"gopkg.in/mgo.v2/bson"
)
type LogPublisher struct {
Printer func(v ...interface{})
}
func (pub *LogPublisher) Emit(doormanId bson.ObjectId, doorman shared.AsDoormanUpdatePayloader) error {
if data, err := doorman.AsDoormanUpdatePayload(); err != nil {
return err
} else {
pub.Printer(doormanId.Hex(), " : ", string(data))
return nil
}
}
func (pub *LogPublisher) Init() error {
pub.Printer = log.Println
return nil
}
+5
View File
@@ -0,0 +1,5 @@
package logpublisher
import "testing"
func TestLogPublisher(t *testing.T) {}
+25
View File
@@ -0,0 +1,25 @@
package mongoconnector
//var mongoClient int
import (
"github.com/didiercrunch/doormanserver/shared"
mgo "gopkg.in/mgo.v2"
)
var mongoClient *mgo.Session
var DB *mgo.Database
func initMongo() error {
if mongoClient != nil {
return nil
}
var err error
mongoClient, err = mgo.Dial(shared.GetParams().Mongo.Url)
if err != nil {
return err
}
DB = mongoClient.DB(shared.GetParams().Mongo.DatabaseName)
return nil
}
+71
View File
@@ -0,0 +1,71 @@
package mongoconnector
import (
"github.com/didiercrunch/doormanserver/shared"
"github.com/didiercrunch/doormanserver/doormen"
mgo "gopkg.in/mgo.v2"
"gopkg.in/mgo.v2/bson"
)
type MongoDBConnector struct {
Collection *mgo.Collection
}
func NewMongoDBConnector() (*MongoDBConnector, error) {
if err := initMongo(); err != nil {
return nil, err
}
c := new(MongoDBConnector)
params := shared.GetParams()
c.Collection = DB.C(params.Mongo.DoormanDefinitionCollection)
err := c.Collection.EnsureIndex(mgo.Index{Key: []string{"name"}, Unique: true})
return c, err
}
func (c *MongoDBConnector) Exists(wab *doormen.DoormanDefinition) bool {
err := c.Collection.Find(bson.M{"name": wab.Name}).One(&struct{}{})
return err != mgo.ErrNotFound
}
func (c *MongoDBConnector) ExistsId(id bson.ObjectId) bool {
err := c.Collection.FindId(id).One(&struct{}{})
return err != mgo.ErrNotFound
}
func (c *MongoDBConnector) Save(wab *doormen.DoormanDefinition) error {
return c.Collection.Insert(wab)
}
func (c *MongoDBConnector) GetAllDoormen() <-chan *doormen.DoormanId {
ch := make(chan *doormen.DoormanId)
go func() {
iter := c.Collection.Find(nil).Iter()
for res := new(doormen.DoormanId); iter.Next(res); res = new(doormen.DoormanId) {
ch <- res
}
if err := iter.Err(); err != nil {
ch <- &doormen.DoormanId{Id: "", Name: err.Error()}
}
close(ch)
}()
return ch
}
func (c *MongoDBConnector) GetDoorman(id bson.ObjectId) (*doormen.DoormanDefinition, error) {
doorman := new(doormen.DoormanDefinition)
if err := c.Collection.FindId(id).One(doorman); err == mgo.ErrNotFound {
return nil, nil
} else {
return doorman, err
}
}
func (c *MongoDBConnector) DeleteDoorman(id bson.ObjectId) error {
return c.Collection.RemoveId(id)
}
func (c *MongoDBConnector) Update(wab *doormen.DoormanDefinition) error {
return c.Collection.Update(&bson.M{"_id": wab.Id}, &bson.M{"$set": wab})
}
+41
View File
@@ -0,0 +1,41 @@
package nanomsgpublisher
import (
"errors"
"log"
"github.com/didiercrunch/doormanserver/shared"
"github.com/gdamore/mangos"
"github.com/gdamore/mangos/protocol/pub"
"github.com/gdamore/mangos/transport/ipc"
"github.com/gdamore/mangos/transport/tcp"
"gopkg.in/mgo.v2/bson"
)
type NanoMsgPublisher struct {
Url string
sock mangos.Socket
}
func (p *NanoMsgPublisher) Init() error {
var err error
if p.sock, err = pub.NewSocket(); err != nil {
return errors.New("can't listen on pub socket:" + err.Error())
}
p.sock.AddTransport(ipc.NewTransport())
p.sock.AddTransport(tcp.NewTransport())
if err = p.sock.Listen(shared.GetParams().NanoMsg.NanoMsgUrl); err != nil {
return errors.New("can't listen on pub socket: %s" + err.Error())
}
return nil
}
func (p *NanoMsgPublisher) Emit(doormanId bson.ObjectId, doorman shared.AsDoormanUpdatePayloader) error {
log.Println("Emit for ", doormanId.Hex())
if d, err := doorman.AsDoormanUpdatePayload(); err != nil {
return err
} else {
return p.sock.Send(d)
}
}
+38
View File
@@ -0,0 +1,38 @@
package nsqpublisher
import (
"github.com/bitly/go-nsq"
"github.com/didiercrunch/doormanserver/shared"
"gopkg.in/mgo.v2/bson"
)
type NsqPublisher struct {
Config *nsq.Config
Producer *nsq.Producer
}
func (pub *NsqPublisher) InitConfig() {
pub.Config = nsq.NewConfig()
}
func (pub *NsqPublisher) Init() error {
var err error
pub.InitConfig()
pub.Producer, err = nsq.NewProducer(shared.GetParams().NSQ.NSQLookupdUrl, pub.Config)
return err
}
func (pub *NsqPublisher) Stop() error {
pub.Producer.Stop()
return nil
}
func (pub *NsqPublisher) Emit(doormanId bson.ObjectId, doorman shared.AsDoormanUpdatePayloader) error {
topic := doormanId.Hex()
if data, err := doorman.AsDoormanUpdatePayload(); err != nil {
return err
} else {
return pub.Producer.Publish(topic, data)
}
}
+45
View File
@@ -0,0 +1,45 @@
---
# This is union web site params' file. The file is in yaml format. The file
# should be auto-documented
# the port where the union web site will run
port: 1999
# the host name. 0.0.0.0 to share the service with everyone on the network.
# localhost for running on localhost without the possibility to share the
# service
host: 0.0.0.0 #localhost
# determine which message queue to use
message_queue: nanomsg # nanomsg # nsq
# determine which database to use
database: in_memory # mongo
################################################################################
# all the parameters relative to nsq message-queue. To use nsq as message
# queue you need to speficy "nsq" in the "message_queue" key at top level.
nsq:
nsq_lookupd_url: 127.0.0.1:4150
# all the parameters related to mongodb. To use mongodb as your database you
# need to specify "mongodb" in the "database" key at top level
mongo:
# the url where to get mongodb
url: localhost:27017
# the name of the database to use
database_name: doorman
# the name of the collection where all the street can be found
doorman_definition_collection: doorman_definitions
# all the parameters relative to nanomsg message-queue
nanomsg:
nanomsg_url: tcp://127.0.0.1:40899
+52
View File
@@ -0,0 +1,52 @@
package main
import (
"log"
"github.com/didiercrunch/doormanserver/logpublisher"
"github.com/didiercrunch/doormanserver/nanomsgpublisher"
"github.com/didiercrunch/doormanserver/nsqpublisher"
"github.com/didiercrunch/doormanserver/shared"
"gopkg.in/mgo.v2/bson"
)
type Publisher interface {
Emit(doormanId bson.ObjectId, doorman shared.AsDoormanUpdatePayloader) error
Init() error
}
var publisher Publisher
func initNSQPublisher() {
publisher = new(nsqpublisher.NsqPublisher)
if err := publisher.Init(); err != nil {
log.Panic(err)
return
}
}
func initNanoMsgPublisher() {
publisher = new(nanomsgpublisher.NanoMsgPublisher)
if err := publisher.Init(); err != nil {
log.Panic(err)
return
}
}
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")
} else {
publisher = &logpublisher.LogPublisher{log.Println}
log.Println("using stdout as message queue")
}
}
func init() {
initPublisher(shared.GetParams())
}
+7
View File
@@ -0,0 +1,7 @@
package main
import "testing"
func TestInitPublisher(t *testing.T) {
}
+33
View File
@@ -0,0 +1,33 @@
package main
import (
"fmt"
"log"
"net/http"
"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
}
func GetAddressToServe() string {
params := shared.GetParams()
return params.Host + ":" + strconv.Itoa(params.Port)
}
func main() {
r := createMuxRouter()
http.Handle("/", r)
address := GetAddressToServe()
fmt.Println("server running at", address)
if err := http.ListenAndServe(address, createMuxRouter()); err != nil {
log.Fatal(err)
}
}
+5
View File
@@ -0,0 +1,5 @@
package shared
type AsDoormanUpdatePayloader interface {
AsDoormanUpdatePayload() ([]byte, error)
}
+100
View File
@@ -0,0 +1,100 @@
package shared
import (
"io/ioutil"
"log"
"strings"
goyaml "gopkg.in/yaml.v2"
)
var PARAM_FILE string = "params.yml"
var params *Params
type MongoParams struct {
Url string `yaml:"url,omitempty"`
DatabaseName string `yaml:"database_name,omitempty"`
DoormanDefinitionCollection string `yaml:"doorman_definition_collection,omitempty"`
}
type NSQParams struct {
NSQLookupdUrl string `yaml:"nsq_lookupd_url,omitempty"`
}
type NanoMsgParams struct {
NanoMsgUrl string `yaml:"nanomsg_url,omitempty"`
}
type Params struct {
Port int `yaml:"port,omitempty"`
Host string `yaml:"host,omitempty"`
Mongo *MongoParams `yaml:"mongo,omitempty"`
NSQ *NSQParams `yaml:"nsq,omitempty"`
NanoMsg *NanoMsgParams `yaml:"nanomsg,omitempty"`
Database string `yaml:"database,omitempty"`
MessageQueue string `yaml:"message_queue,omitempty"`
}
func (p *Params) UseMongoDb() bool {
db := strings.ToLower(p.Database)
return db == "mongo" || db == "mongodb"
}
func (p *Params) UseNSQ() bool {
m := strings.ToLower(p.MessageQueue)
return m == "nsq"
}
func (p *Params) UseNanomsg() bool {
m := strings.ToLower(p.MessageQueue)
return m == "nanomsg"
}
func (p *Params) InsertDefaultValues() *Params {
if p.Host == "" {
p.Host = "localhost"
}
if p.Port == 0 {
p.Port = 1999
}
if p.Mongo == nil {
p.Mongo = new(MongoParams)
}
if p.NSQ == nil {
p.NSQ = new(NSQParams)
}
return p
}
func LoadFromYamlFile(fileName string) (*Params, error) {
if !Exists(fileName) {
return new(Params).InsertDefaultValues(), nil
}
data, err := ioutil.ReadFile(fileName)
if err != nil {
return nil, err
}
return LoadFromYamlData(data)
}
func LoadFromYamlData(data []byte) (*Params, error) {
var err error
p := new(Params)
if err = goyaml.Unmarshal(data, p); err != nil {
return nil, err
}
return p.InsertDefaultValues(), nil
}
func GetParams() *Params {
return params
}
func init() {
var err error
if params, err = LoadFromYamlFile(PARAM_FILE); err != nil {
log.Panic(err)
}
}
+142
View File
@@ -0,0 +1,142 @@
package shared
import (
"testing"
)
const yaml_params = `
---
port: 9999
host: 0.0.0.0 #localhost
# all the params relative to mongodb
mongo:
# the url where to get mongodb
url: localhost:1234
# the name of the database to use
database_name: doorman
# the name of the collection where all the street can be found
doorman_definition_collection: doorman_definitions
# all the parameters relative to nsq message-queue
nsq:
# the url where to find nsq lookupd url
nsq_lookupd_url: 127.0.0.1:4161
# all the parameters relative to nanomsg message-queue
nanomsg:
nanomsg_url: ipc:///tmp/doorman.ipc
`
func TestLoadFromYamlData(t *testing.T) {
p, err := LoadFromYamlData([]byte(yaml_params))
if err != nil {
t.Error(err)
return
}
if p.Port != 9999 {
t.Error("port")
}
if p.Host != "0.0.0.0" {
t.Error("host")
}
}
func TestMongoParams(t *testing.T) {
p, err := LoadFromYamlData([]byte(yaml_params))
if err != nil {
t.Error(err)
return
}
if p.Mongo.Url != "localhost:1234" {
t.Error("mongo.url")
}
if p.Mongo.DatabaseName != "doorman" {
t.Error("mongo.DatabaseName")
}
if p.Mongo.DoormanDefinitionCollection != "doorman_definitions" {
t.Error("Mongo.DoormanDefinitionCollection", p.Mongo.DoormanDefinitionCollection)
}
}
func TestNSQParams(t *testing.T) {
p, err := LoadFromYamlData([]byte(yaml_params))
if err != nil {
t.Error(err)
return
}
if p.NSQ.NSQLookupdUrl != "127.0.0.1:4161" {
t.Error("Nsq.NSQLookupdUrl", p.NSQ.NSQLookupdUrl)
}
}
func TestNanoMsgParams(t *testing.T) {
p, err := LoadFromYamlData([]byte(yaml_params))
if err != nil {
t.Error(err)
return
}
if p.NanoMsg.NanoMsgUrl != "ipc:///tmp/doorman.ipc" {
t.Error("NanoMsgParams.NanoMsgUrl", p.NanoMsg.NanoMsgUrl)
}
}
func TestMinimalParams(t *testing.T) {
params := "---\n"
p, err := LoadFromYamlData([]byte(params))
if err != nil {
t.Error(err)
}
if p.Mongo.Url != "" {
t.Error()
}
if p.Port != 1999 {
t.Error("bad default port", p.Port)
}
if p.Host != "localhost" {
t.Error("bad default host")
}
if p.MessageQueue != "" {
t.Error("message queue")
}
if p.Database != "" {
t.Error("database")
}
}
func TestUseMongoDb(t *testing.T) {
p := &Params{}
if p.Database = ""; p.UseMongoDb() {
t.Error()
}
if p.Database = "mOnGo"; !p.UseMongoDb() {
t.Error()
}
if p.Database = "monGoDb"; !p.UseMongoDb() {
t.Error()
}
}
func TestUseNSQ(t *testing.T) {
p := &Params{}
if p.MessageQueue = ""; p.UseNSQ() {
t.Error()
}
if p.MessageQueue = "nSq"; !p.UseNSQ() {
t.Error()
}
}
func TestUseNanoMsg(t *testing.T) {
p := &Params{}
if p.MessageQueue = ""; p.UseNanomsg() {
t.Error()
}
if p.MessageQueue = "nAnOmSg"; !p.UseNanomsg() {
t.Error()
}
}
+12
View File
@@ -0,0 +1,12 @@
package shared
import "os"
func Exists(name string) bool {
if _, err := os.Stat(name); err != nil {
if os.IsNotExist(err) {
return false
}
}
return true
}
+7
View File
@@ -0,0 +1,7 @@
logs/*
!.gitkeep
node_modules/
bower_components/
tmp
.DS_Store
.idea
+26
View File
@@ -0,0 +1,26 @@
{
"name": "angular-requirejs-seed",
"description": "AngularJS seed project modified for requireJS support.",
"version": "0.2.0",
"homepage": "https://github.com/tnajdek/angular-requirejs-seed",
"license": "MIT",
"ignore": [
"**/.*",
"node_modules",
"bower_components",
"test",
"tests"
],
"dependencies": {
"angular": "~1.2.1",
"angular-route": "~1.2.1",
"angular-scenario": "~1.2.1",
"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"
}
}
+35
View File
@@ -0,0 +1,35 @@
define [
"angularAMD"
"angular-route"
"foundation"
], (angularAMD) ->
app = angular.module("webapp", ["ngRoute", "mm.foundation"])
app.config ($routeProvider) ->
$routeProvider.when("/", angularAMD.route(
templateUrl: "partials/home.html"
controller: "HomeCtrl"
controllerUrl: "controllers/homeCtrl"
)).when("/apispec", angularAMD.route(
templateUrl: "partials/apispec.html"
controller: "ApispecCtrl"
controllerUrl: "controllers/apispecCtrl"
)).when("/newdoorman", angularAMD.route(
templateUrl: "partials/newdoorman.html"
controller: "NewDoormanCtrl"
controllerUrl: "controllers/newdoormanCtrl"
)).when("/doormen", angularAMD.route(
templateUrl: "partials/doormen.html"
controller: "DoormenCtrl"
controllerUrl: "controllers/doormenCtrl"
)).when("/doormen/error/:error/:id", angularAMD.route(
templateUrl: "partials/doormanError.html"
controller: "DoormanErrorCtrl"
controllerUrl: "controllers/doormanErrorCtrl"
)).when("/doormen/:id", angularAMD.route(
templateUrl: "partials/doorman.html"
controller: "DoormanCtrl"
controllerUrl: "controllers/doormanCtrl"
)).otherwise redirectTo: "/"
return
angularAMD.bootstrap(app)
@@ -0,0 +1,28 @@
define ["app", "angularAMD"], (app, angularAMD) ->
app.controller "ApispecCtrl", ($scope) ->
$scope.methods =
get: "GET"
post: "POST"
angularAMD.directive "wabMethod", () ->
directive =
restrict: 'E',
scope:
method: '@'
link: (scope, element) ->
scope.methods =
get: "GET"
put: "PUT"
delete: "DELETE"
post: "POST"
template: '<code style="width:5em; display: block; text-align: center;">{{ methods[method] }}</code>'
return directive
angularAMD.directive "wabApi", () ->
directive =
restrict: 'E',
scope:
endpoint: '@'
template: '<a>/api{{ endpoint }}</a>'
return directive
@@ -0,0 +1,30 @@
define ["app"], (app) ->
app.controller "DoormanCtrl", ["$scope", "$http","$routeParams","$location", ($scope, $http, $routeParams, $location) ->
$scope.doorman = {values: []}
$scope.apiurl = "/api/doormen/#{$routeParams.id}"
$http.get($scope.apiurl).then((res) ->
$scope.doorman = res.data
).catch((res) ->
if res.status == 404
$location.path("/doormen/error/#{res.status}/#{$routeParams.id}")
)
$scope.deleteDoorman = () ->
console.log("not yet implemented")
$scope.validate = ()->
ret = 0
for val in $scope.doorman.values
ret += val.probability
return Math.abs(ret - 1.0) < 0.000001
$scope.updateDoorman = () ->
if !$scope.validate()
return
$http.put($scope.apiurl, $scope.doorman).then((ret)->
console.log "update"
).catch((res) ->
d = 9
)
]
return
@@ -0,0 +1,6 @@
define ["app"], (app) ->
app.controller "DoormanErrorCtrl", ["$scope","$routeParams", ($scope, $routeParams) ->
$scope.id = $routeParams.id
$scope.error = Number($routeParams.error)
]
return
@@ -0,0 +1,9 @@
define ["app"], (app) ->
app.controller "DoormenCtrl",[ "$scope", "$http", ($scope, $http) ->
$scope.doormen = []
$http.get("/api/doormen").then (res) ->
$scope.doormen = res.data.doormen
return
]
return
@@ -0,0 +1,6 @@
define ["app"], (app) ->
app.controller "HomeCtrl", ($scope) ->
$scope.message = "message from home ctrl"
return
return
@@ -0,0 +1,57 @@
epsilon = 0.0001
define ["app"], (app) ->
app.controller "NewDoormanCtrl", ["$scope", "$http", "$location", ($scope, $http, $location) ->
$scope.error = ""
$scope.value = {}
$scope.payload =
name: ""
values: []
$scope.isValidValue = () ->
return $scope.value.name and $scope.value.probability
$scope.addNewValue = () ->
if !$scope.isValidValue()
return
$scope.payload.values.push($scope.value)
$scope.value = {name: "", probability: 0}
$scope.removeValue = (idx) ->
$scope.payload.values.splice(idx, 1);
$scope.getProbabilityRemaining = () ->
ret = 0
for v in $scope.payload.values
ret += v.probability
return 1 - ret
$scope.redirectToDoorman = (doormanLocation) ->
$http.get(doormanLocation).then (res) ->
id = res.data.id
return $location.url("/doormen/#{id}")
$scope.createDoorman = () ->
if not $scope.isDoormanValid()
return
$http.post("/api/doormen", $scope.payload).
then((res, s) ->
$scope.redirectToDoorman(res.headers("location"))
).catch((res) ->
$scope.error = res.data
)
$scope.isDoormanValid = () ->
if not $scope.payload.name
return false
if $scope.payload.values.length < 2
return false
if Math.abs($scope.getProbabilityRemaining()) > epsilon
return false
return true
return
]
return
@@ -0,0 +1,6 @@
define ["app"], (app) ->
app.controller "sideBarCtrl", ($scope) ->
$scope.message = "Message from MyCtrl1"
return
return
+20
View File
@@ -0,0 +1,20 @@
require.config
baseUrl: "js"
# alias libraries paths. Must set 'angular'
paths:
"angular" : "/bower_components/angular/angular.min"
"angular-route": "/bower_components/angular-route/angular-route.min"
"angularAMD" : "/bower_components/angularAMD/angularAMD.min"
"foundation" : "/bower_components/angular-foundation/mm-foundation-tpls"
# Add angular modules that does not support AMD out of the box, put it in a shim
shim:
"angularAMD": ["angular"]
"angular-route": ["angular"]
"foundation" : ["angular"]
# kick start application
deps: ["app"]
+50
View File
@@ -0,0 +1,50 @@
var bower = require('gulp-bower');
var gulp = require('gulp');
var coffee = require('gulp-coffee');
var help = require('gulp-task-listing');
var clean = require('gulp-clean');
var sass = require('gulp-sass');
var rjs = require("gulp-rjs");
gulp.task('help', help);
gulp.task('bower', function() {
return bower()
.pipe(gulp.dest('bower_components/'))
});
gulp.task('sass', ['bower'], function () {
return gulp.src('./scss/*.scss')
.pipe(sass({includePaths: ['bower_components/foundation/scss']}))
.pipe(gulp.dest('./css'));
});
gulp.task('coffee', ['bower'], function() {
return gulp.src('coffee/**/*.coffee')
.pipe(coffee({bare: true}))
.pipe(gulp.dest('js/'))
});
gulp.task('clean', function(){
return gulp.src(["js/", "bower_components/", "css/"], {read: false})
.pipe(clean())
});
gulp.task('rjs', ['coffee'], function(){
return gulp.src('coffee/**/*.coffee')
.pipe(coffee())
.pipe(gulp.dest('./dist'))
.pipe(rjs({baseUrl:'.'}))
});
gulp.task('default', ['bower', 'sass', 'coffee']);
gulp.task('dev', ['bower', 'sass', 'coffee'], function(){
gulp.watch('coffee/**/*.coffee', ['coffee']);
gulp.watch('./scss/*.scss', ['sass']);
});
View File
Whitespace-only changes.
+44
View File
@@ -0,0 +1,44 @@
<!DOCTYPE html>
<html>
<head>
<title>doorman</title>
<link rel="stylesheet" href="css/style.css">
<script data-main="/js/main" src="/bower_components/requirejs/require.js"></script>
</head>
<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">
<a class="right-off-canvas-toggle menu-icon" ><span></span></a>
</section>
</nav>
<aside class="right-off-canvas-menu">
<ul class="off-canvas-list">
<li><a></a></li>
<li><a href="#/doormen">doormen</a></li>
<li><a href="#/newdoorman">new doorman</a></li>
<li><a href="#/apispec">api specification</a></li>
<li><a href="#/serverspec">server specification</a></li>
</ul>
</aside>
<section class="main-section">
<div class="large-12 small-12 columns" ng-view>
</section>
<a class="exit-off-canvas"></a>
</div>
</div>
</body>
</html>
+14
View File
@@ -0,0 +1,14 @@
{
"dependencies": {
"gulp": "~3.8.9",
"gulp-bower": "0.0.7",
"gulp-clean": "~0.3.1",
"gulp-coffee": "~2.2.0",
"gulp-sass": "^1.2.4",
"gulp-task-listing": "~0.3.0",
"node-sass": "~1.2.3"
},
"devDependencies": {
"gulp-rjs": "^0.1.0"
}
}
+46
View File
@@ -0,0 +1,46 @@
<div class="row wab-apispect">
<div class="columns large-12">
<h2>Api specification</h2>
</div>
</div>
<div class="row wab-apispect">
<table class="columns large-12">
<thead>
<tr>
<th >api endpoint</th>
<th>method</th>
<th >description</th>
</tr>
</thead>
<tbody>
<tr>
<td><wab-api endpoint="/doormen"></wab-api></td>
<td><wab-method method="get"></wab-method></td>
<td>Get the list of all the public doormen.</td>
</tr>
<tr>
<td><wab-api endpoint="/doormen"></wab-api></td>
<td><wab-method method="post"></wab-method></td>
<td>Create a new doorman.</td>
</tr>
<tr>
<td><wab-api endpoint="/doormen/:id"></wab-api></td>
<td><wab-method method="get"></wab-method></td>
<td>Get the doorman</td>
</tr>
<tr>
<td><wab-api endpoint="/doormen/:id"></wab-api></td>
<td><wab-method method="put"></wab-method></td>
<td>Update the doorman with new probabilities</td>
</tr>
<tr>
<td><wab-api endpoint="/doormen/:id"></wab-api></td>
<td><wab-method method="delete"></wab-method></td>
<td>Delete the doorman</td>
</tr>
</tbody>
</table>
</div>
+29
View File
@@ -0,0 +1,29 @@
<div class="row">
<div class="columns large-12">
<h2>{{ doorman.name }} </h2>
</div>
</div>
<div class="row">
<div class="columns large-12">
<span>doorman id: <code>{{ doorman.id }}</code></span>
</div>
</div>
<div class="row" ng-repeat="value in doorman.values">
<div class="columns large-9 small-10">
<label >name<input type="text" placeholder="value name" ng-value="value.name" disabled></label>
</div>
<div class="columns large-3 small-2">
<label >probability<input type="number" placeholder="value probability" ng-model="value.probability" min="0" max="1" step="0.01"></label>
</div>
</div>
<div class="row">
<div class="large-12 small-12 columns">
<ul class="button-group">
<li><a ng-click="updateDoorman()" class="small button" ng-class="{disabled: !validate()}">Update</a></li>
<li><a ng-click="deleteDoorman()" class="small button">Delete</a></li>
</ul>
</div>
</div>
+6
View File
@@ -0,0 +1,6 @@
<div class="row">
<div class="columns large-12">
<span>Error {{ error }} getting doorman {{ id }}</span>
</div>
</div>
+13
View File
@@ -0,0 +1,13 @@
<div class="row">
<div class="columns large-12">
<h2>doormen</h2>
</div>
</div>
<div class="row">
<div class="columns large-12">
<ul>
<li ng-repeat="doorman in doormen"> <a href="#/doormen/{{ doorman.id }}"> {{ doorman.name }} </a></li>
</ul>
</div>
</div>
+7
View File
@@ -0,0 +1,7 @@
<h2>Welcome to the world best a/b testing library</h2>
<div class="row">
<div class="large-12 columns">
{{ message }}
</div>
</div>
+54
View File
@@ -0,0 +1,54 @@
<h2>New Doorman</h2>
<form>
<div class="row">
<div class="columns large-12">
<label >name<input type="text" placeholder="unique doorman name" ng-model="payload.name"></label>
</div>
</div>
<div class="row" ng-repeat="value in payload.values">
<div class="columns large-8">
<label >value<input type="text" placeholder="value name" ng-model="value.name"></label>
</div>
<div class="columns large-3">
<label >probability<input type="number" placeholder="value probability" ng-model="value.probability" min="0" max="1" step="0.01"></label>
</div>
<div class="columns large-1">
<br>
<a class="button round tiny" ng-click="removeValue( $index )">-</a>
</div>
</div>
<div class="row">
<div class="columns large-8">
<label >name<input type="text" placeholder="value name" ng-model="value.name"></label>
</div>
<div class="columns large-3">
<label >probability<input type="number" placeholder="value probability" ng-model="value.probability" min="0" max="1" step="0.01"></label>
</div>
<div class="columns large-1">
<br>
<a class="button round tiny" ng-class="{disabled: !isValidValue()}" ng-click="addNewValue()">+</a>
</div>
</div>
<div class="row" ng-show="error">
<div class="columns large-12">
<code style="width:100%; height:100%; display:block; margin-bottom: 1em;">{{ error }}</code>
</div>
</div>
<div class="row">
<div class="large-11 columns">
<button ng-click="createDoorman()" ng-class="{disabled: !isDoormanValid()}">create</button>
</div>
<div class="large-1 columns">
<span >propability remaning: {{ getProbabilityRemaining() }}</span>
</div>
</div>
</form>
+21
View File
@@ -0,0 +1,21 @@
@import "settings";
@import url(http://fonts.googleapis.com/css?family=Orbitron:400,900,500,700);
h1, h2{
font-family: 'Orbitron', sans-serif;
}
.wab-sidebar{
background: $ghost;
h1{
text-align: center;
color: $primary-color;
}
}
.wab-apispect{
h2{
text-align: center;
}
}
File diff suppressed because it is too large. Load diff
+41
View File
@@ -0,0 +1,41 @@
@import "settings";
@import "foundation";
@import "doorman";
// Or selectively include components
// @import
// "foundation/components/accordion",
// "foundation/components/alert-boxes",
// "foundation/components/block-grid",
// "foundation/components/breadcrumbs",
// "foundation/components/button-groups",
// "foundation/components/buttons",
// "foundation/components/clearing",
// "foundation/components/dropdown",
// "foundation/components/dropdown-buttons",
// "foundation/components/flex-video",
// "foundation/components/forms",
// "foundation/components/grid",
// "foundation/components/inline-lists",
// "foundation/components/joyride",
// "foundation/components/keystrokes",
// "foundation/components/labels",
// "foundation/components/magellan",
// "foundation/components/orbit",
// "foundation/components/pagination",
// "foundation/components/panels",
// "foundation/components/pricing-tables",
// "foundation/components/progress-bars",
// "foundation/components/reveal",
// "foundation/components/side-nav",
// "foundation/components/split-buttons",
// "foundation/components/sub-nav",
// "foundation/components/switch",
// "foundation/components/tables",
// "foundation/components/tabs",
// "foundation/components/thumbs",
// "foundation/components/tooltips",
// "foundation/components/top-bar",
// "foundation/components/type",
// "foundation/components/offcanvas",
// "foundation/components/visibility";
+20
View File
@@ -0,0 +1,20 @@
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)
}