Bläddra i källkod

Initial commit

Luis Figueiredo 8 år sedan
incheckning
e7907a8a3c

+ 1 - 0
.gitignore

@@ -0,0 +1 @@
+dist.tar.gz

+ 11 - 0
Dockerfile.build

@@ -0,0 +1,11 @@
+FROM golang:1.8
+
+ADD src/sampleapp $GOPATH/src/sampleapp
+WORKDIR $GOPATH
+RUN go get -v sampleapp/cmd/server
+
+RUN go test sampleapp/customersvc
+RUN go test sampleapp
+
+RUN CGO_ENABLED=0 go build -o bin/sampleapp sampleapp/cmd/server
+CMD tar -czf - bin/sampleapp

+ 5 - 0
Dockerfile.dist

@@ -0,0 +1,5 @@
+FROM scratch
+ADD dist.tar.gz /app/
+WORKDIR /app
+EXPOSE 8080
+ENTRYPOINT ["./sampleapp"]

+ 19 - 0
Makefile

@@ -0,0 +1,19 @@
+
+DIST=dist.tar.gz
+
+all:  $(DIST)
+
+$(DIST):
+	docker build --rm -t sampleapp-build -f Dockerfile.build .
+	docker run sampleapp-build > $(DIST)
+
+dist: $(DIST)
+	docker build -t sampleapp -f Dockerfile.dist .
+
+clean:
+	rm $(DIST)
+
+
+.PHONY: clean dist
+
+

+ 25 - 0
src/sampleapp/cmd/server/main.go

@@ -0,0 +1,25 @@
+package main
+
+import (
+	"sampleapp"
+	"sampleapp/customersvc"
+
+	mgo "gopkg.in/mgo.v2-unstable"
+)
+
+func main() {
+
+	sess, err := mgo.Dial("localhost")
+	if err != nil {
+		panic(err)
+	}
+	db := sess.DB("sample_watt")
+
+	s := sampleapp.New()
+	// This kind of injection is not a good practice
+	s.CustomerSVC = &customersvc.Service{db}
+
+	// Initialize things like DB
+
+	s.ListenAndServe(":8080")
+}

+ 51 - 0
src/sampleapp/customerctrl.go

@@ -0,0 +1,51 @@
+/*
+ * This could be in another package, controllers, but golang tend to be simgle folder per concern
+ */
+
+package sampleapp
+
+import (
+	"encoding/json"
+	"log"
+	"net/http"
+	"sampleapp/customersvc"
+
+	"gopkg.in/mgo.v2-unstable/bson"
+
+	"github.com/gorilla/mux"
+)
+
+type CustomerController struct {
+	svc customersvc.Servicer
+}
+
+func (cc *CustomerController) GetCustomerHandler(w http.ResponseWriter, r *http.Request) {
+	vars := mux.Vars(r)
+	id := vars["id"]
+
+	if !bson.IsObjectIdHex(id) {
+		w.WriteHeader(http.StatusBadRequest)
+		return
+	}
+	cust, err := cc.svc.FetchByID(id) // This should be in userservice
+	if err == customersvc.ErrNotFound {
+		httpErr(w, http.StatusNotFound, "Not found")
+		return
+	}
+	if err != nil {
+		httpErr(w, http.StatusInternalServerError, "Error fetching customer", err)
+		return
+	}
+	json.NewEncoder(w).Encode(cust)
+}
+
+func (cc *CustomerController) PostCustomerHandler(w http.ResponseWriter, r *http.Request) {
+	log.Println("Post")
+	defer r.Body.Close()
+
+	cust := &customersvc.Customer{}
+	json.NewDecoder(r.Body).Decode(&cust)
+
+	log.Println("Received:", cust)
+	//Store user from body
+}

+ 19 - 0
src/sampleapp/customersvc/customer.go

@@ -0,0 +1,19 @@
+package customersvc
+
+const (
+	TypLandLine = iota
+	TypMobile
+)
+
+// Customer data structure
+type Customer struct {
+	ID    string `bson:"_id,omitempty"` // Could be anything else like UUID, or simple int
+	Name  string
+	Phone Phone
+}
+
+// Phone data structure
+type Phone struct {
+	Type   byte // 255 types
+	Number string
+}

+ 60 - 0
src/sampleapp/customersvc/customersvc.go

@@ -0,0 +1,60 @@
+package customersvc
+
+import (
+	"errors"
+
+	"github.com/google/uuid"
+
+	mgo "gopkg.in/mgo.v2-unstable"
+	"gopkg.in/mgo.v2-unstable/bson"
+)
+
+const (
+	CustomerColl = "customer"
+)
+
+var (
+	ErrNotFound = errors.New("Not Found!")
+)
+
+type Servicer interface {
+	Store(c *Customer) error
+	FetchByID(id string) (*Customer, error)
+}
+
+// Service customer service
+type Service struct {
+	DS *mgo.Database
+}
+
+// New or Init?
+
+func (s *Service) Init() {
+
+}
+
+// Store customer
+func (s *Service) Store(c *Customer) error {
+
+	c.ID = uuid.New().String() // could collide
+
+	return s.coll().Insert(c)
+}
+
+// FetchById gets by object id
+func (s *Service) FetchByID(id string) (*Customer, error) {
+
+	c := Customer{}
+	err := s.coll().Find(bson.M{"_id": id}).One(&c)
+	if err == mgo.ErrNotFound {
+		return nil, ErrNotFound // This is because the implementers of this service should be database agnostic
+	}
+	if err != nil {
+		return nil, err
+	}
+	return &c, nil
+}
+
+func (s *Service) coll() *mgo.Collection {
+	return s.DS.C("customer")
+}

+ 44 - 0
src/sampleapp/customersvc/customersvc_test.go

@@ -0,0 +1,44 @@
+package customersvc_test
+
+import (
+	"os"
+	"sampleapp/customersvc"
+	. "sampleapp/testutils"
+	"testing"
+
+	mgo "gopkg.in/mgo.v2-unstable"
+)
+
+func MustPrepareDB() *mgo.Database {
+	dbURL := os.Getenv("MONGODB")
+	if dbURL == "" {
+		return nil
+	}
+	sess, err := mgo.Dial(dbURL)
+	if err != nil {
+		panic(err) // MustPrepareDB
+	}
+
+	db := sess.DB("sample_test")
+
+	return db
+}
+
+func TestStore(t *testing.T) {
+	db := MustPrepareDB()
+	if db == nil {
+		t.Skip("Database not ready")
+	}
+
+	cs := &customersvc.Service{db}
+
+	c := customersvc.Customer{
+		Name: "Luis",
+		Phone: customersvc.Phone{
+			Type:   customersvc.TypMobile,
+			Number: "+5545991121214",
+		},
+	}
+	err := cs.Store(&c)
+	FailNotEq(t, err, nil)
+}

+ 13 - 0
src/sampleapp/routes.go

@@ -0,0 +1,13 @@
+package sampleapp
+
+func (s *SampleApp) InitRoutes() {
+
+	// Customer routes
+	cc := &CustomerController{s.CustomerSVC}
+	s.router.Methods("GET").Path("/customer/{id}").HandlerFunc(LogHandler(cc.GetCustomerHandler))
+	s.router.Methods("POST").Path("/customer").HandlerFunc(LogHandler(cc.PostCustomerHandler))
+
+	// Prepare handlers and middleware
+	s.router.HandleFunc("/", LogHandler(nil))
+
+}

+ 41 - 0
src/sampleapp/sampleapp.go

@@ -0,0 +1,41 @@
+package sampleapp
+
+import (
+	"log"
+	"net/http"
+	"sampleapp/customersvc"
+
+	"github.com/gorilla/mux"
+)
+
+// SampleApp
+type SampleApp struct {
+	// Datastore for this app
+	//sess *mgo.Session
+	//db   *mgo.Database
+
+	// Services
+	CustomerSVC customersvc.Servicer
+
+	// golang http router
+	router *mux.Router
+}
+
+func New() *SampleApp {
+	return &SampleApp{}
+}
+
+// Router returns a lazy initialized router
+func (s *SampleApp) Router() *mux.Router {
+	if s.router == nil {
+		s.router = mux.NewRouter()
+		s.InitRoutes()
+	}
+	return s.router
+}
+
+// ListenAndServe serves
+func (s *SampleApp) ListenAndServe(addr string) {
+	log.Println("Starting http at", addr)
+	http.ListenAndServe(addr, s.Router())
+}

+ 84 - 0
src/sampleapp/sampleapp_test.go

@@ -0,0 +1,84 @@
+package sampleapp_test
+
+import (
+	"bytes"
+	"encoding/json"
+	"net/http"
+	"net/http/httptest"
+	"sampleapp"
+	"sampleapp/customersvc"
+	. "sampleapp/testutils"
+	"testing"
+)
+
+var (
+	sampleUser = customersvc.Customer{ // omit id
+		ID:   "58ff6b3f673686f96801707b",
+		Name: "Luis",
+		Phone: customersvc.Phone{
+			Type:   customersvc.TypMobile,
+			Number: "+5545991121214",
+		},
+	}
+)
+
+func TestGetCustomerBadID(t *testing.T) {
+	s := sampleapp.New()
+	s.CustomerSVC = &CustomerSVCMock{}
+	ts := httptest.NewServer(s.Router())
+
+	res, err := http.Get(ts.URL + "/customer/1")
+	if err != nil {
+		t.Fatal(err)
+	}
+	FailNotEq(t, res.StatusCode, 400)
+
+}
+func TestGetCustomer(t *testing.T) {
+	s := sampleapp.SampleApp{CustomerSVC: &CustomerSVCMock{}}
+
+	ts := httptest.NewServer(s.Router())
+
+	res, err := http.Get(ts.URL + "/customer/58ff6b3f673686f96801707b")
+	if err != nil {
+		t.Fatal(err)
+	}
+	FailNotEq(t, res.StatusCode, 200)
+	c := customersvc.Customer{}
+	defer res.Body.Close()
+
+	json.NewDecoder(res.Body).Decode(&c)
+	FailNotEq(t, c.Name, "Luis")
+}
+
+func TestPostCustomer(t *testing.T) {
+	s := sampleapp.SampleApp{CustomerSVC: &CustomerSVCMock{}}
+	ts := httptest.NewServer(s.Router())
+
+	var bufData bytes.Buffer
+
+	newCustomer := sampleUser // Copy
+
+	json.NewEncoder(&bufData).Encode(newCustomer)
+	res, err := http.Post(ts.URL+"/customer", "application/json", &bufData)
+	FailNotEq(t, err, nil)
+
+	FailNotEq(t, res.StatusCode, 200)
+
+}
+
+// CustomerSVCMock customer service mock
+type CustomerSVCMock struct{}
+
+func (cc *CustomerSVCMock) Store(c *customersvc.Customer) error {
+
+	return nil
+}
+
+func (cc *CustomerSVCMock) FetchByID(id string) (*customersvc.Customer, error) {
+	if id == sampleUser.ID {
+		dbUser := sampleUser // copy
+		return &dbUser, nil
+	}
+	return nil, customersvc.ErrNotFound
+}

+ 26 - 0
src/sampleapp/testutils/testutils.go

@@ -0,0 +1,26 @@
+package testutils_test
+
+import (
+	"fmt"
+	"runtime"
+	"strings"
+	"testing"
+)
+
+func FailNotEq(t *testing.T, v1 interface{}, v2 interface{}) {
+	if v1 != v2 {
+		t.Fatalf("%s - Not Equal %+v != %+v", line(), v1, v2)
+	}
+}
+func FailEq(t *testing.T, v1 interface{}, v2 interface{}) {
+	if v1 == v2 {
+		t.Fatalf("%s - Equal %+v == %+v", line(), v1, v2)
+	}
+}
+
+// Helper to show where it failed
+func line() string {
+	_, file, line, _ := runtime.Caller(2)
+	si := strings.LastIndex(file, "/") + 1
+	return fmt.Sprintf("%s:%d", file[si:], line)
+}

+ 42 - 0
src/sampleapp/utils.go

@@ -0,0 +1,42 @@
+package sampleapp
+
+import (
+	"encoding/json"
+	"fmt"
+	"log"
+	"net/http"
+)
+
+// httpResponse helper
+func httpErr(w http.ResponseWriter, code int, msg ...interface{}) {
+	w.WriteHeader(code)
+
+	ret := map[string]interface{}{
+		"err": fmt.Sprintln(msg...),
+	}
+	json.NewEncoder(w).Encode(ret)
+}
+
+// -------------------
+// GO http Handler  for writing logs
+//
+type LogWriter struct {
+	http.ResponseWriter
+	statusCode int
+}
+
+func (lh *LogWriter) WriteHeader(code int) {
+	lh.statusCode = code
+	lh.ResponseWriter.WriteHeader(code)
+}
+
+func LogHandler(next http.HandlerFunc) http.HandlerFunc {
+
+	return func(w http.ResponseWriter, r *http.Request) {
+		lh := &LogWriter{w, 200} // Default 200
+		if next != nil {
+			next(lh, r)
+		}
+		log.Printf("%s (%d) - %s", r.Method, lh.statusCode, r.URL.Path)
+	}
+}