Skip to content

Commit

Permalink
Initial commit
Browse files Browse the repository at this point in the history
  • Loading branch information
Kervin Christianata committed Feb 26, 2022
0 parents commit 2f86a45
Show file tree
Hide file tree
Showing 125 changed files with 16,019 additions and 0 deletions.
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
.envrc
bin/
83 changes: 83 additions & 0 deletions Makefile
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
include .envrc

# ==================================================================================== #
# HELPERS
# ==================================================================================== #

## help: print this help message
.PHONY: help
help:
@echo 'Usage:'
@sed -n 's/^##//p' ${MAKEFILE_LIST} | column -t -s ':' | sed -e 's/^/ /'

.PHONY: confirm
confirm:
@echo -n 'Are you sure? [y/N] ' && read ans && [ $${ans: -N} = y ]

# ==================================================================================== #
# DEVELOPMENT
# ==================================================================================== #

## run/api: run the cmd/api application
.PHONY: run/api
run/api:
go run ./cmd/api -db-dsn=${DB_DSN}

## db/psql: connect to the database using psql
.PHONY: db/psql
db/psql:
psql ${DB_DSN}

## db/migrations/new name=$1: create a new database migration
.PHONY: db/migrations/new
db/migrations/new:
@echo 'Creating migration files for ${name}'
migrate create -seq -ext=.sql -dir=./migrations ${name}

## db/migrations/up: apply all up database migrations
.PHONY: db/migrations/up
db/migrations/up: confirm
@echo 'Running up migrations...'
migrate -path=./migrations -database ${DB_DSN} up

# ==================================================================================== #
# QUALITY CONTROL
# ==================================================================================== #

## audit: tidy dependencies and format, vet and test all code
.PHONY: audit
audit: vendor
@echo 'Formatting code...'
go fmt ./...
@echo 'Vetting code...'
go vet ./...
staticcheck ./...
@echo 'Running tests...'
go test -race -vet=off ./...

## vendor: tidy and vendor dependencies
.PHONY: vendor
vendor:
@echo 'Tidying and verifying module dependencies...'
go mod tidy
go mod verify
@echo 'Vendoring dependencies...'
go mod vendor

# ==================================================================================== #
# BUILD
# ==================================================================================== #

current_time = $(shell date --iso-8601=seconds)
git_description = $(shell git describe --always --dirty)
linker_flags = '-s -X main.buildTime=${current_time} -X main.version=${git_description}'

## build/api: build the cmd/api application
.PHONY: build/api
build/api:
@echo 'Building cmd/api...'
go build -ldflags=${linker_flags} -o=./bin/api ./cmd/api
GOOS=linux GOARCH=amd64 go build -ldflags=${linker_flags} -o=./bin/linux_amd64/api ./cmd/api

# migrate -path= ./migrations -database ${GREENLIGHT_DB_DSN} up
# migrate -path= ./migrations -database "postgres://postgres@localhost:5432/greenlight?sslmode=disable" up
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
# kin-api
29 changes: 29 additions & 0 deletions cmd/api/context.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
package main

import (
"context"
"net/http"

"github.com/kervinch/internal/data"
)

type contextKey string

const userContextKey = contextKey("user")

// The contextSetUser() method returns a new copy of the request with the provided
// User struct added to the context. Note that we use our userContextKey constant as the
// key.
func (app *application) contextSetUser(r *http.Request, user *data.User) *http.Request {
ctx := context.WithValue(r.Context(), userContextKey, user)
return r.WithContext(ctx)
}

func (app *application) contextGetUser(r *http.Request) *data.User {
user, ok := r.Context().Value(userContextKey).(*data.User)
if !ok {
panic("missing user value in request context")
}

return user
}
83 changes: 83 additions & 0 deletions cmd/api/errors.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
package main

import (
"fmt"
"net/http"
)

func (app *application) logError(r *http.Request, err error) {
app.logger.PrintError(err, map[string]string{
"request_method": r.Method,
"request_url": r.URL.String(),
})
}

func (app *application) errorResponse(w http.ResponseWriter, r *http.Request, status int, message interface{}) {
err := app.writeJSON(w, status, http.StatusText(status), message, nil)
if err != nil {
app.logError(r, err)
w.WriteHeader(500)
}
}

func (app *application) serverErrorResponse(w http.ResponseWriter, r *http.Request, err error) {
app.logError(r, err)

message := "the server encountered a problem and could not process your request"
app.errorResponse(w, r, http.StatusInternalServerError, message)
}

func (app *application) notFoundResponse(w http.ResponseWriter, r *http.Request) {
message := "the requested resource could not be found"
app.errorResponse(w, r, http.StatusNotFound, message)
}

func (app *application) methodNotAllowedResponse(w http.ResponseWriter, r *http.Request) {
message := fmt.Sprintf("the %s method is not supported for this resource", r.Method)
app.errorResponse(w, r, http.StatusMethodNotAllowed, message)
}

func (app *application) badRequestResponse(w http.ResponseWriter, r *http.Request, err error) {
app.errorResponse(w, r, http.StatusBadRequest, err.Error())
}

func (app *application) failedValidationResponse(w http.ResponseWriter, r *http.Request, errors map[string]string) {
app.errorResponse(w, r, http.StatusUnprocessableEntity, errors)
}

func (app *application) editConflictResponse(w http.ResponseWriter, r *http.Request) {
message := "unable to update the record due to an edit conflict, please try again"
app.errorResponse(w, r, http.StatusConflict, message)
}

func (app *application) rateLimitExceededResponse(w http.ResponseWriter, r *http.Request) {
message := "rate limit exceeded"
app.errorResponse(w, r, http.StatusTooManyRequests, message)
}

func (app *application) invalidCredentialsResponse(w http.ResponseWriter, r *http.Request) {
message := "invalid authentication credentials"
app.errorResponse(w, r, http.StatusUnauthorized, message)
}

func (app *application) invalidAuthenticationTokenResponse(w http.ResponseWriter, r *http.Request) {
w.Header().Set("WWW-Authenticate", "Bearer")

message := "invalid or missing authentication token"
app.errorResponse(w, r, http.StatusUnauthorized, message)
}

func (app *application) authenticationRequiredResponse(w http.ResponseWriter, r *http.Request) {
message := "you must be authenticated to access this resource"
app.errorResponse(w, r, http.StatusUnauthorized, message)
}

func (app *application) invactiveAccountResponse(w http.ResponseWriter, r *http.Request) {
message := "your user account must be activated to access this resource"
app.errorResponse(w, r, http.StatusForbidden, message)
}

func (app *application) notPermittedResponse(w http.ResponseWriter, r *http.Request) {
messsage := "your user account doesn't have the necessary permissions to access this resource"
app.errorResponse(w, r, http.StatusForbidden, messsage)
}
21 changes: 21 additions & 0 deletions cmd/api/healthcheck.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
package main

import (
"net/http"
)

// Declare a handler which writes a plain-text response with information about the // application status, operating environment and version.
func (app *application) healthcheckHandler(w http.ResponseWriter, r *http.Request) {
env := envelope{
"status": "available",
"system_info": map[string]string{
"environment": app.config.env,
"version": version,
},
}

err := app.writeJSON(w, http.StatusOK, http.StatusText(http.StatusOK), env, nil)
if err != nil {
app.serverErrorResponse(w, r, err)
}
}
161 changes: 161 additions & 0 deletions cmd/api/helpers.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,161 @@
package main

import (
"encoding/json"
"errors"
"fmt"
"io"
"net/http"
"net/url"
"strconv"
"strings"

"github.com/julienschmidt/httprouter"
"github.com/kervinch/internal/validator"
)

type envelope map[string]interface{}

func (app *application) readIDParam(r *http.Request) (int64, error) {
params := httprouter.ParamsFromContext(r.Context())

id, err := strconv.ParseInt(params.ByName("id"), 10, 64)
if err != nil || id < 1 {
return 0, errors.New("invalid id parameter")
}

return id, nil
}

func (app *application) writeJSON(w http.ResponseWriter, status int, message string, data interface{}, headers http.Header) error {
result := make(map[string]interface{})

result["code"] = status
result["message"] = message
if status >= 400 {
result["error"] = data
} else {
result["data"] = data
}

js, err := json.Marshal(result)
if err != nil {
return err
}

js = append(js, '\n')

for key, value := range headers {
w.Header()[key] = value
}

w.Header().Set("Content-Type", "application/json")
w.WriteHeader(status)
w.Write(js)

return nil
}

func (app *application) readJSON(w http.ResponseWriter, r *http.Request, dst interface{}) error {
maxBytes := 1_048_576
r.Body = http.MaxBytesReader(w, r.Body, int64(maxBytes))

dec := json.NewDecoder(r.Body)
dec.DisallowUnknownFields()

err := dec.Decode(dst)
if err != nil {
var syntaxError *json.SyntaxError
var unmarshalTypeError *json.UnmarshalTypeError
var invalidUnmarshalError *json.InvalidUnmarshalError

switch {
case errors.As(err, &syntaxError):
return fmt.Errorf("body contains badly-formed JSON (at character %d)", syntaxError.Offset)

case errors.Is(err, io.ErrUnexpectedEOF):
return errors.New("body contains badly-formed JSON")

case errors.As(err, &unmarshalTypeError):
if unmarshalTypeError.Field != "" {
return fmt.Errorf("body contains incorrect JSON type for field %q", unmarshalTypeError.Field)
}
return fmt.Errorf("body contains incorrect JSON type (at character %d)", unmarshalTypeError.Offset)

case errors.Is(err, io.EOF):
return errors.New("body must not be empty")

case strings.HasPrefix(err.Error(), "json: unknown field "):
fieldName := strings.TrimPrefix(err.Error(), "json: unknown field ")
return fmt.Errorf("body contains unknown key %s", fieldName)

case err.Error() == "http: request body too large":
return fmt.Errorf("body must not be larger than %d bytes", maxBytes)

case errors.As(err, &invalidUnmarshalError):
panic(err)

default:
return err
}
}

err = dec.Decode(&struct{}{})
if err != io.EOF {
return errors.New("body must only contain a single JSON value")
}

return nil
}

func (app *application) readStrings(qs url.Values, key string, defaultValue string) string {
s := qs.Get(key)

if s == "" {
return defaultValue
}

return s
}

func (app *application) readCSV(qs url.Values, key string, defaultValue []string) []string {
csv := qs.Get(key)

if csv == "" {
return defaultValue
}

return strings.Split(csv, ",")
}

func (app *application) readInt(qs url.Values, key string, defaultValue int, v *validator.Validator) int {
s := qs.Get(key)

if s == "" {
return defaultValue
}

i, err := strconv.Atoi(s)
if err != nil {
v.AddError(key, "must be an integer value")
return defaultValue
}

return i
}

func (app *application) background(fn func()) {
app.wg.Add(1)

go func() {
app.wg.Done()

defer func() {
if err := recover(); err != nil {
app.logger.PrintError(fmt.Errorf("%s", err), nil)
}
}()

fn()
}()
}
Loading

0 comments on commit 2f86a45

Please sign in to comment.