Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 4 additions & 1 deletion .env-template
Original file line number Diff line number Diff line change
@@ -1,4 +1,7 @@
#GODEBUG=gctrace=1

JOKE_SERVICE_URL=https://api.chucknorris.io/jokes/random
PORT=3000
JOKE_SERVICE_PORT=3000
MONGO_HOST=mongodb://localhost:27017
MONGO_DATABASE=template
MONGO_COLLECTION=beer
4 changes: 2 additions & 2 deletions .realize.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -13,10 +13,10 @@ schema:
commands:
install:
status: true
dir: cmd
method: go build -o output/app ./cmd
run:
status: true
method: cmd
method: output/app
watcher:
paths:
- /
Expand Down
5 changes: 2 additions & 3 deletions build/Dockerfile.dev
Original file line number Diff line number Diff line change
Expand Up @@ -10,9 +10,8 @@ RUN go get -u github.com/mgechev/revive
# realize: watcher and live reloading
RUN go get github.com/oxequa/realize

WORKDIR /go/src/ctco-dev/$PROJECT
# enable Go Modules
ENV GO111MODULE=on
WORKDIR /app/src/ctco-dev/$PROJECT

# no CGO (see https://github.com/golang/go/issues/28065)
ENV CGO_ENABLED=0

Expand Down
6 changes: 3 additions & 3 deletions cmd/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -24,8 +24,8 @@ func main() {
log.WithCtx(rootCtx).Panicf("env vars error: '%v'", err)
}

someApp := app.New(env)
addr := fmt.Sprintf(":%d", env.Port)
someApp := app.New(rootCtx, env)
addr := fmt.Sprintf(":%d", env.JokeServicePort)
handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
reqID := uuid.NewV4().String()[0:8]
reqCtx := log.NewContext(rootCtx, logrus.Fields{"reqID": reqID})
Expand All @@ -34,6 +34,6 @@ func main() {
someApp.ServeHTTP(w, r.WithContext(reqCtx))
})

log.WithCtx(rootCtx).Infof("Server is running at: http://localhost:%d", env.Port)
log.WithCtx(rootCtx).Infof("Server is running at: http://localhost:%d", env.JokeServicePort)
log.WithCtx(rootCtx).Fatal(http.ListenAndServe(addr, handler))
}
10 changes: 10 additions & 0 deletions docker-compose.dev.yml
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,16 @@ services:
- ./cmd:/go/src/ctco-dev/go-api-template/cmd
ports:
- 3000:3000
depends_on:
- mongo
environment:
- JOKE_SERVICE_URL
- JOKE_SERVICE_PORT
- MONGO_HOST=mongodb://mongo:27017
- MONGO_DATABASE
- MONGO_COLLECTION
tty: true
mongo:
image: mongo
volumes:
- /data/db
4 changes: 4 additions & 0 deletions docker-compose.yml
Original file line number Diff line number Diff line change
Expand Up @@ -8,4 +8,8 @@ services:
- 3000:3000
environment:
- JOKE_SERVICE_URL
- JOKE_SERVICE_PORT
- MONGO_HOST
- MONGO_DATABASE
- MONGO_COLLECTION
tty: true
9 changes: 9 additions & 0 deletions go.mod
Original file line number Diff line number Diff line change
@@ -1,12 +1,21 @@
module github.com/ctco-dev/go-api-template

require (
github.com/go-stack/stack v1.8.0 // indirect
github.com/golang/snappy v0.0.1 // indirect
github.com/google/go-cmp v0.2.0 // indirect
github.com/kelseyhightower/envconfig v1.3.0
github.com/kr/pretty v0.1.0 // indirect
github.com/pkg/errors v0.8.1
github.com/satori/go.uuid v1.2.0
github.com/sirupsen/logrus v1.1.0
github.com/tidwall/pretty v0.0.0-20180105212114-65a9db5fad51 // indirect
github.com/xdg/scram v0.0.0-20180814205039-7eeb5667e42c // indirect
github.com/xdg/stringprep v1.0.0 // indirect
go.mongodb.org/mongo-driver v1.0.0
golang.org/x/crypto v0.0.0-20180927165925-5295e8364332 // indirect
golang.org/x/sync v0.0.0-20190227155943-e225da77a7e6 // indirect
golang.org/x/sys v0.0.0-20180928133829-e4b3c5e90611 // indirect
golang.org/x/text v0.3.0 // indirect
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 // indirect
)
27 changes: 20 additions & 7 deletions go.sum
Original file line number Diff line number Diff line change
@@ -1,29 +1,42 @@
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/go-stack/stack v1.8.0 h1:5SgMzNM5HxrEjV0ww2lTmX6E2Izsfxas4+YHWRs3Lsk=
github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY=
github.com/golang/snappy v0.0.1 h1:Qgr9rKW7uDUkrbSmQeiDsGa8SjGyCOGtuasMWwvp2P4=
github.com/golang/snappy v0.0.1/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q=
github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M=
github.com/kelseyhightower/envconfig v1.3.0 h1:IvRS4f2VcIQy6j4ORGIf9145T/AsUB+oY8LyvN8BXNM=
github.com/kelseyhightower/envconfig v1.3.0/go.mod h1:cccZRl6mQpaq41TPp5QxidR+Sa3axMbJDNb//FQX6Gg=
github.com/konsorten/go-windows-terminal-sequences v0.0.0-20180402223658-b729f2633dfe h1:CHRGQ8V7OlCYtwaKPJi3iA7J+YdNKdo8j7nG5IgDhjs=
github.com/konsorten/go-windows-terminal-sequences v0.0.0-20180402223658-b729f2633dfe/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
github.com/kr/pretty v0.1.0 h1:L/CwN0zerZDmRFUapSPitk6f+Q3+0za1rQkzVuMiMFI=
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE=
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
github.com/mongodb/mongo-go-driver v1.0.0 h1:aq055NT+Xu6ta/f7D51gIbLHIZwM0Gwzt9RHfmrzs6A=
github.com/mongodb/mongo-go-driver v1.0.0/go.mod h1:NK/HWDIIZkaYsnYa0hmtP443T5ELr0KDecmIioVuuyU=
github.com/pkg/errors v0.8.1 h1:iURUrRGxPUNPdy5/HRSm+Yj6okJ6UtLINN0Q9M4+h3I=
github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/satori/go.uuid v1.2.0 h1:0uYX9dsZ2yD7q2RtLRtPSdGDWzjeM3TbMJP9utgA0ww=
github.com/satori/go.uuid v1.2.0/go.mod h1:dA0hQrYB0VpLJoorglMZABFdXlWrHn1NEOzdhQKdks0=
github.com/sirupsen/logrus v1.1.0 h1:65VZabgUiV9ktjGM5nTq0+YurgTyX+YI2lSSfDjI+qU=
github.com/sirupsen/logrus v1.1.0/go.mod h1:zrgwTnHtNr00buQ1vSptGe8m1f/BbgsPukg8qsT7A+A=
github.com/stretchr/testify v1.2.2 h1:bSDNvY7ZPG5RlJ8otE/7V6gMiyenm9RtJ7IUVIAoJ1w=
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
github.com/tidwall/pretty v0.0.0-20180105212114-65a9db5fad51/go.mod h1:XNkn88O1ChpSDQmQeStsy+sBenx6DDtFZJxhVysOjyk=
github.com/xdg/scram v0.0.0-20180814205039-7eeb5667e42c h1:u40Z8hqBAAQyv+vATcGgV0YCnDjqSL7/q/JyPhhJSPk=
github.com/xdg/scram v0.0.0-20180814205039-7eeb5667e42c/go.mod h1:lB8K/P019DLNhemzwFU4jHLhdvlE6uDZjXFejJXr49I=
github.com/xdg/stringprep v1.0.0 h1:d9X0esnoa3dFsV0FG35rAT0RIhYFlPq7MiP+DW89La0=
github.com/xdg/stringprep v1.0.0/go.mod h1:Jhud4/sHMO4oL310DaZAKk9ZaJ08SJfe+sJh0HrGL1Y=
go.mongodb.org/mongo-driver v0.3.0 h1:DASSc8s5ayctU+AWYPnCTXFWevhMA3HblGxGK2HICQA=
go.mongodb.org/mongo-driver v0.3.0/go.mod h1:u7ryQJ+DOzQmeO7zB6MHyr8jkEQvC8vH7qLUO4lqsUM=
go.mongodb.org/mongo-driver v1.0.0 h1:KxPRDyfB2xXnDE2My8acoOWBQkfv3tz0SaWTRZjJR0c=
go.mongodb.org/mongo-driver v1.0.0/go.mod h1:u7ryQJ+DOzQmeO7zB6MHyr8jkEQvC8vH7qLUO4lqsUM=
golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
golang.org/x/crypto v0.0.0-20180927165925-5295e8364332 h1:hvQVdF6P9DX4OiKA5tpehlG6JsgzmyQiThG7q5Bn3UQ=
golang.org/x/crypto v0.0.0-20180927165925-5295e8364332/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
golang.org/x/sync v0.0.0-20190227155943-e225da77a7e6 h1:bjcUS9ztw9kFmmIxJInhon/0Is3p+EHBKNgquIzo1OI=
golang.org/x/sync v0.0.0-20190227155943-e225da77a7e6/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20180928133829-e4b3c5e90611 h1:O33LKL7WyJgjN9CvxfTIomjIClbd/Kq86/iipowHQU0=
golang.org/x/sys v0.0.0-20180928133829-e4b3c5e90611/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 h1:qIbj1fsPNlZgppZ+VLlY7N33q108Sa+fhmuc+sWQYwY=
golang.org/x/text v0.3.0 h1:g61tztE5qeGQ89tm6NTjjM9VPIm088od1l6aSorWRWg=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
31 changes: 22 additions & 9 deletions internal/app/app.go
Original file line number Diff line number Diff line change
@@ -1,39 +1,52 @@
package app

import (
"context"
"net/http"
"path"
"strings"

mongobeer "github.com/ctco-dev/go-api-template/internal/db"

"github.com/ctco-dev/go-api-template/internal/joke"
)

//Specification is a container of app config parameters
type Specification struct {
JokeServiceURL string `split_words:"true" required:"true" default:"https://api.chucknorris.io/jokes/random"`
Port int `split_words:"true" required:"true" default:"3000"`
JokeServiceURL string `split_words:"true" required:"true" default:"https://api.chucknorris.io/jokes/random"`
JokeServicePort int `split_words:"true" required:"true" default:"3000"`
MongoHost string `split_words:"true" required:"true" default:"mongodb://localhost:27017"`
MongoDatabase string `split_words:"true" required:"true" default:"template"`
MongoCollection string `split_words:"true" required:"true" default:"beer"`
}

// App implements a sample http service
type app struct {
jokeHandler *jokeHandler
beerHandler *beerHandler
}

// New creates a new application
func New(env Specification) http.Handler {
return &app{jokeHandler: &jokeHandler{
client: joke.NewChuckNorrisAPIClient(env.JokeServiceURL),
}}
func New(ctx context.Context, env Specification) http.Handler {
return &app{
jokeHandler: &jokeHandler{client: joke.NewChuckNorrisAPIClient(env.JokeServiceURL)},
beerHandler: &beerHandler{
repository: mongobeer.NewRepo(ctx, env.MongoHost, env.MongoDatabase, env.MongoCollection),
},
}
}

func (a *app) ServeHTTP(w http.ResponseWriter, r *http.Request) {
var head string
head, r.URL.Path = ShiftPath(r.URL.Path)
if head == "joke" {
switch head {
case "joke":
a.jokeHandler.ServeHTTP(w, r)
return
case "beer":
a.beerHandler.ServeHTTP(w, r)
default:
http.Error(w, "Not Found", http.StatusNotFound)
}
http.Error(w, "Not Found", http.StatusNotFound)
}

// ShiftPath splits off the first component of p, which will be cleaned of
Expand Down
129 changes: 129 additions & 0 deletions internal/app/beer-handler.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,129 @@
package app

import (
"context"
"encoding/json"
"net/http"

"github.com/ctco-dev/go-api-template/internal/beer"

"github.com/ctco-dev/go-api-template/internal/log"
)

// JokeHandler handles /joke route
type beerHandler struct {
repository beer.Repository
}

func (b *beerHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
var head string
head, _ = ShiftPath(r.URL.Path)

if head != "" {
http.Error(w, "Not Found", http.StatusNotFound)
return
}

switch r.Method {
case "GET":
b.handleGet(w, r)
case "PUT":
b.handlePut(w, r)
case "DELETE":
b.handleDelete(w, r)
default:
http.Error(w, "Only GET, PUT and DELETE are allowed", http.StatusMethodNotAllowed)
}
}

func (b *beerHandler) handleGet(w http.ResponseWriter, r *http.Request) {
ctx := r.Context()

ids, ok := r.URL.Query()["id"]

if !ok || len(ids[0]) < 1 {
getAllBeers(ctx, w, b.repository)
return
}

getOneBeer(ctx, w, ids[0], b.repository)
}

func (b *beerHandler) handlePut(w http.ResponseWriter, r *http.Request) {
ctx := r.Context()

names, ok := r.URL.Query()["name"]

if !ok || len(names[0]) < 1 {
http.Error(w, "Beer name is missing.", http.StatusInternalServerError)
return
}

writeBeer(ctx, w, names[0], b.repository)
}

func (b *beerHandler) handleDelete(w http.ResponseWriter, r *http.Request) {
ctx := r.Context()

ids, ok := r.URL.Query()["id"]

if !ok || len(ids[0]) < 1 {
http.Error(w, "Beer id is missing.", http.StatusInternalServerError)
return
}

removeBeer(ctx, w, ids[0], b.repository)
}

func getOneBeer(ctx context.Context, w http.ResponseWriter, id string, reader beer.Reader) {
beer, err := reader.Read(ctx, id)
if err != nil {
http.Error(w, "Can't get a beer.", http.StatusInternalServerError)
return
}

writeResult(ctx, w, beer)
}

func getAllBeers(ctx context.Context, w http.ResponseWriter, reader beer.AllReader) {
beers, err := reader.ReadAll(ctx)
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}

writeResult(ctx, w, beers)
}

func writeBeer(ctx context.Context, w http.ResponseWriter, name string, writer beer.Writer) {
id, err := writer.Write(ctx, beer.Beer{Name: name})
if err != nil {
http.Error(w, "Can't write a beer.", http.StatusInternalServerError)
return
}

writeResult(ctx, w, id)
}

func removeBeer(ctx context.Context, w http.ResponseWriter, id string, remover beer.Remover) {
err := remover.Remove(ctx, id)
if err != nil {
http.Error(w, "Can't delete a beer.", http.StatusInternalServerError)
return
}

writeResult(ctx, w, "success")
}

func writeResult(ctx context.Context, w http.ResponseWriter, data interface{}) {
bytes, err := json.Marshal(data)
if err != nil {
http.Error(w, "Can't encode response.", http.StatusInternalServerError)
return
}

w.WriteHeader(http.StatusOK)
w.Header().Set("Content-Type", "application/json")
w.Write(bytes)
log.WithCtx(ctx).Info("I'm done")
}
2 changes: 1 addition & 1 deletion internal/app/joke-handler.go
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@ func (j *jokeHandler) handleGet(w http.ResponseWriter, r *http.Request) {
log.WithCtx(ctx).Info("I'm trying to get new joke")
jokeResp, err := j.client.GetJoke(ctx)
if err != nil {
http.Error(w, "Can't get error", http.StatusInternalServerError)
http.Error(w, "Can't get a joke", http.StatusInternalServerError)
return
}

Expand Down
42 changes: 42 additions & 0 deletions internal/beer/beer.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
package beer

import (
"context"
)

// ID is string alias type for User id
type ID = string

// Beer represents a beer
type Beer struct {
ID ID
Name string
}

// Reader reads beer by id
type Reader interface {
Read(context.Context, ID) (*Beer, error)
}

// AllReader reads all beers
type AllReader interface {
ReadAll(context.Context) ([]*Beer, error)
}

// Writer writes a new beer and returns its id
type Writer interface {
Write(context.Context, Beer) (ID, error)
}

// Remover removes a beer by id
type Remover interface {
Remove(context.Context, ID) error
}

// Repo is an interface for a beer repository
type Repository interface {
Reader
AllReader
Writer
Remover
}
Loading