Initial bootrap of application #10
@@ -0,0 +1,17 @@
|
|||||||
|
name: Supermarket-API CI Pipeline
|
||||||
|
on:
|
||||||
|
pull_request:
|
||||||
|
branches:
|
||||||
|
- main
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
unit-test:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v2
|
||||||
|
- uses: actions/setup-go@v1
|
||||||
|
with:
|
||||||
|
go-version: 1.x
|
||||||
|
- run: |
|
||||||
|
export PATH=$PATH:$(go env GOPATH)/bin # temporary fix. See https://github.com/actions/setup-go/issues/14
|
||||||
|
make unit-test
|
||||||
+4
-1
@@ -12,4 +12,7 @@
|
|||||||
*.out
|
*.out
|
||||||
|
|
||||||
# Dependency directories (remove the comment below to include it)
|
# Dependency directories (remove the comment below to include it)
|
||||||
# vendor/
|
vendor/
|
||||||
|
|
||||||
|
# Environment file
|
||||||
|
.env
|
||||||
|
|||||||
@@ -0,0 +1,20 @@
|
|||||||
|
dev:
|
||||||
|
docker-compose up
|
||||||
|
|
||||||
|
unit-test:
|
||||||
|
go test -short ./...
|
||||||
|
|
||||||
|
integration-test:
|
||||||
|
go test -integration ./...
|
||||||
|
|
||||||
|
all-test:
|
||||||
|
go test ./...
|
||||||
|
|
||||||
|
build:
|
||||||
|
go build -o supermarket-api cmd/api/*.go
|
||||||
|
|
||||||
|
run:
|
||||||
|
go run cmd/api/*.go
|
||||||
|
|
||||||
|
clean:
|
||||||
|
rm ./supermarket-api
|
||||||
@@ -1 +1,17 @@
|
|||||||
# supermarket-api
|
# supermarket-api
|
||||||
|
|
||||||
|
Supermarket-API is a service providing functionality for cataloging produce and their prices.
|
||||||
|
|
||||||
|
## Getting Started
|
||||||
|
|
||||||
|
To start `supermarket-api` you will need to create an `.env` file at `cmd/api/.env`. A convenience example with sane defaults is located at `cmd/api/example.env` and you can create your environment file by running:
|
||||||
|
|
||||||
|
```
|
||||||
|
~$ cp cmd/api/example.env cmd/api/.env
|
||||||
|
```
|
||||||
|
|
||||||
|
Supermarket-API has a Makefile with commonly ran commands. To use the Makefile append the command to `make` in your terminal:
|
||||||
|
|
||||||
|
```
|
||||||
|
~$ make run
|
||||||
|
```
|
||||||
|
|||||||
Executable
BIN
Binary file not shown.
@@ -0,0 +1,18 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/joho/godotenv"
|
||||||
|
"github.com/kelseyhightower/envconfig"
|
||||||
|
)
|
||||||
|
|
||||||
|
type config struct {
|
||||||
|
Env string `default:"dev"`
|
||||||
|
APIPort int `default:3000`
|
||||||
|
LogLevel string `default:"debug"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func load() (cfg config, err error) {
|
||||||
|
_ = godotenv.Load("cmd/api/.env")
|
||||||
|
err = envconfig.Process("", &cfg)
|
||||||
|
return
|
||||||
|
}
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
ENV: dev
|
||||||
|
APIPORT: 3000
|
||||||
|
LOGLEVEL: debug
|
||||||
@@ -0,0 +1,62 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"log"
|
||||||
|
"os"
|
||||||
|
"os/signal"
|
||||||
|
"syscall"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/davidlick/supermarket-api/internal/http"
|
||||||
|
"github.com/sirupsen/logrus"
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
cfg config
|
||||||
|
logger *logrus.Logger
|
||||||
|
err error
|
||||||
|
)
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
cfg, err = load()
|
||||||
|
if err != nil {
|
||||||
|
log.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
ll, err := logrus.ParseLevel(cfg.LogLevel)
|
||||||
|
if err != nil {
|
||||||
|
log.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
logger = logrus.New()
|
||||||
|
logger.SetLevel(ll)
|
||||||
|
}
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
server := http.NewServer(cfg.APIPort, logger, cfg.Env)
|
||||||
|
|
||||||
|
// Allow app to listen for OS Interrupts and SIGTERMS.
|
||||||
|
serverErrors := make(chan error, 1)
|
||||||
|
osSignals := make(chan os.Signal, 1)
|
||||||
|
signal.Notify(osSignals, os.Interrupt, syscall.SIGTERM)
|
||||||
|
|
||||||
|
go func() {
|
||||||
|
serverErrors <- server.Run()
|
||||||
|
}()
|
||||||
|
|
||||||
|
// Handling for server errors and OS signals.
|
||||||
|
select {
|
||||||
|
case err := <-serverErrors:
|
||||||
|
log.Fatal("error starting server: %w", err.Error())
|
||||||
|
case <-osSignals:
|
||||||
|
log.Println("starting server shutdown...")
|
||||||
|
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
|
||||||
|
defer cancel()
|
||||||
|
|
||||||
|
err := server.Shutdown(ctx)
|
||||||
|
if err != nil {
|
||||||
|
log.Fatalf("error shutting down http server: %v", err.Error())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,10 @@
|
|||||||
|
module github.com/davidlick/supermarket-api
|
||||||
|
|
||||||
|
go 1.16
|
||||||
|
|
||||||
|
require (
|
||||||
|
github.com/go-chi/chi v1.5.4 // indirect
|
||||||
|
github.com/joho/godotenv v1.3.0 // indirect
|
||||||
|
github.com/kelseyhightower/envconfig v1.4.0 // indirect
|
||||||
|
github.com/sirupsen/logrus v1.8.1 // indirect
|
||||||
|
)
|
||||||
@@ -0,0 +1,13 @@
|
|||||||
|
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||||
|
github.com/go-chi/chi v1.5.4 h1:QHdzF2szwjqVV4wmByUnTcsbIg7UGaQ0tPF2t5GcAIs=
|
||||||
|
github.com/go-chi/chi v1.5.4/go.mod h1:uaf8YgoFazUOkPBG7fxPftUylNumIev9awIWOENIuEg=
|
||||||
|
github.com/joho/godotenv v1.3.0 h1:Zjp+RcGpHhGlrMbJzXTrZZPrWj+1vfm90La1wgB6Bhc=
|
||||||
|
github.com/joho/godotenv v1.3.0/go.mod h1:7hK45KPybAkOC6peb+G5yklZfMxEjkZhHbwpqxOKXbg=
|
||||||
|
github.com/kelseyhightower/envconfig v1.4.0 h1:Im6hONhd3pLkfDFsbRgu68RDNkGF1r3dvMUtDTo2cv8=
|
||||||
|
github.com/kelseyhightower/envconfig v1.4.0/go.mod h1:cccZRl6mQpaq41TPp5QxidR+Sa3axMbJDNb//FQX6Gg=
|
||||||
|
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||||
|
github.com/sirupsen/logrus v1.8.1 h1:dJKuHgqk1NNQlqoA6BTlM1Wf9DOH3NBjQyu0h9+AZZE=
|
||||||
|
github.com/sirupsen/logrus v1.8.1/go.mod h1:yWOB1SBYBC5VeMP7gHvWumXLIWorT60ONWic61uBYv0=
|
||||||
|
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
|
||||||
|
golang.org/x/sys v0.0.0-20191026070338-33540a1f6037 h1:YyJpGZS1sBuBCzLAR1VEpK193GlqGZbnPFnPV/5Rsb4=
|
||||||
|
golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
package http
|
||||||
|
|
||||||
|
import "errors"
|
||||||
|
|
||||||
|
var (
|
||||||
|
ErrUnknownError = errors.New("unknown error occurred")
|
||||||
|
ErrUnrecognizedCode = errors.New("unrecognized status code")
|
||||||
|
)
|
||||||
@@ -0,0 +1,13 @@
|
|||||||
|
package http
|
||||||
|
|
||||||
|
import "net/http"
|
||||||
|
|
||||||
|
func (s *server) handleHealth(w http.ResponseWriter, r *http.Request) {
|
||||||
|
w.WriteHeader(http.StatusOK)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *server) handleReadiness(w http.ResponseWriter, r *http.Request) {
|
||||||
|
w.WriteHeader(http.StatusOK)
|
||||||
|
return
|
||||||
|
}
|
||||||
@@ -0,0 +1,80 @@
|
|||||||
|
package http
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
"net/http"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/go-chi/chi"
|
||||||
|
"github.com/go-chi/chi/middleware"
|
||||||
|
"github.com/sirupsen/logrus"
|
||||||
|
)
|
||||||
|
|
||||||
|
// server holds configurations and services used by the HTTP server and handlers.
|
||||||
|
type server struct {
|
||||||
|
port int
|
||||||
|
logger *logrus.Logger
|
||||||
|
environment string
|
||||||
|
server *http.Server
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewServer initializes a new server with the required configurations.
|
||||||
|
func NewServer(port int, logger *logrus.Logger, environment string) *server {
|
||||||
|
return &server{
|
||||||
|
port: port,
|
||||||
|
logger: logger,
|
||||||
|
environment: environment,
|
||||||
|
server: &http.Server{
|
||||||
|
Addr: fmt.Sprintf(":%d", port),
|
||||||
|
ReadTimeout: 60 * time.Second,
|
||||||
|
WriteTimeout: 60 * time.Second,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Run builds the routes and starts the server listening on the configured port.
|
||||||
|
func (s *server) Run() error {
|
||||||
|
s.server.Handler = s.buildRoutes()
|
||||||
|
s.logger.Infof("starting server on port: %d", s.port)
|
||||||
|
return s.server.ListenAndServe()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Shutdown calls Shutdown on the http.Server which attempts to gracefully shutdown. If the context deadline is exceeded any cotnext errors are returned.
|
||||||
|
func (s *server) Shutdown(ctx context.Context) error {
|
||||||
|
return s.server.Shutdown(ctx)
|
||||||
|
}
|
||||||
|
|
||||||
|
// buildRoutes configures a router with middleware, headers, and routes.
|
||||||
|
func (s *server) buildRoutes() http.Handler {
|
||||||
|
r := s.configureRouter()
|
||||||
|
|
||||||
|
r.Get("/health", s.handleHealth)
|
||||||
|
r.Get("/ready", s.handleReadiness)
|
||||||
|
|
||||||
|
r.Group(func(r chi.Router) {
|
||||||
|
r.Route("/v1", func(r chi.Router) {
|
||||||
|
// r.Group(s.produceGroup)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
return r
|
||||||
|
}
|
||||||
|
|
||||||
|
// configureRouter returns a configured chi.Router with sane defaults.
|
||||||
|
func (s *server) configureRouter() chi.Router {
|
||||||
|
r := chi.NewRouter()
|
||||||
|
r.Use(middleware.RequestID)
|
||||||
|
r.Use(middleware.RealIP)
|
||||||
|
r.Use(middleware.Logger)
|
||||||
|
r.Use(middleware.Recoverer)
|
||||||
|
r.Use(setResponseHeaders(map[string]string{
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
"Allow-Access-Control-Origin": "*",
|
||||||
|
"Allow-Access-Control-Method": "OPTIONS, GET, POST, DELETE",
|
||||||
|
"Allow-Access-Control-Headers": "Origin, Content-Type, Accept",
|
||||||
|
"Access-Control-Max-Age": "600",
|
||||||
|
}))
|
||||||
|
|
||||||
|
return r
|
||||||
|
}
|
||||||
@@ -0,0 +1,16 @@
|
|||||||
|
package http
|
||||||
|
|
||||||
|
import "net/http"
|
||||||
|
|
||||||
|
// setResponseHeaders is a middleware that accepts a map[string]string of headers:values and sets them for all responses.
|
||||||
|
func setResponseHeaders(headers map[string]string) func(http.Handler) http.Handler {
|
||||||
|
return func(next http.Handler) http.Handler {
|
||||||
|
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
for header, value := range headers {
|
||||||
|
w.Header().Set(header, value)
|
||||||
|
}
|
||||||
|
|
||||||
|
next.ServeHTTP(w, r)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,43 @@
|
|||||||
|
package http
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"encoding/json"
|
||||||
|
"net/http"
|
||||||
|
)
|
||||||
|
|
||||||
|
func (s *server) writeSuccess(ctx context.Context, w http.ResponseWriter, data interface{}, status int) error {
|
||||||
|
if status != 0 {
|
||||||
|
w.WriteHeader(status)
|
||||||
|
}
|
||||||
|
|
||||||
|
if data != nil {
|
||||||
|
err := json.NewEncoder(w).Encode(data)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *server) writeError(ctx context.Context, w http.ResponseWriter, err error, code int) error {
|
||||||
|
if http.StatusText(code) == "" {
|
||||||
|
return ErrUnrecognizedCode
|
||||||
|
}
|
||||||
|
|
||||||
|
if err == nil {
|
||||||
|
err = ErrUnknownError
|
||||||
|
}
|
||||||
|
|
||||||
|
s.logger.Errorf("request failed: %v", err.Error())
|
||||||
|
w.WriteHeader(code)
|
||||||
|
|
||||||
|
res := struct {
|
||||||
|
Message string `json:"message"`
|
||||||
|
}{
|
||||||
|
Message: http.StatusText(code),
|
||||||
|
}
|
||||||
|
|
||||||
|
return json.NewEncoder(w).Encode(res)
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user