diff --git a/.air.toml b/.air.toml new file mode 100644 index 0000000..6b3f3be --- /dev/null +++ b/.air.toml @@ -0,0 +1,46 @@ +root = "." +testdata_dir = "testdata" +tmp_dir = "tmp" + +[build] + args_bin = [] + bin = "tmp\\main.exe" + cmd = "go build -o ./tmp/main.exe ./cmd" + delay = 1000 + exclude_dir = ["assets", "tmp", "vendor", "testdata"] + exclude_file = [] + exclude_regex = ["_test.go"] + exclude_unchanged = false + follow_symlink = false + full_bin = "" + include_dir = [] + include_ext = ["go", "tpl", "tmpl", "html"] + include_file = [] + kill_delay = "0s" + log = "build-errors.log" + poll = false + poll_interval = 0 + post_cmd = [] + pre_cmd = [] + rerun = false + rerun_delay = 500 + send_interrupt = false + stop_on_error = false + +[color] + app = "" + build = "yellow" + main = "magenta" + runner = "green" + watcher = "cyan" + +[log] + main_only = false + time = false + +[misc] + clean_on_exit = false + +[screen] + clear_on_rebuild = false + keep_scroll = true diff --git a/.gitignore b/.gitignore index e33fba0..e9c17b3 100644 --- a/.gitignore +++ b/.gitignore @@ -8,9 +8,12 @@ *.so *.dylib +./vendor +./vendor/* vendor vendor/* .env +/tmp .vscode .idea diff --git a/api/shared/infra/router/routers.go b/api/shared/infra/router/routers.go new file mode 100644 index 0000000..e936b26 --- /dev/null +++ b/api/shared/infra/router/routers.go @@ -0,0 +1,11 @@ +package sharedRouter + +import ( + "context" + + v1Router "github.com/quinntas/go-rest-template/api/shared/infra/router/v1" +) + +func InitRouters(ctx context.Context) { + v1Router.NewV1Router("/api/v1", ctx) +} diff --git a/api/shared/infra/router/v1/router.go b/api/shared/infra/router/v1/router.go new file mode 100644 index 0000000..a43ee65 --- /dev/null +++ b/api/shared/infra/router/v1/router.go @@ -0,0 +1,15 @@ +package v1Router + +import ( + "context" + "net/http" + + healthcheckUseCase "github.com/quinntas/go-rest-template/api/shared/useCases/healthCheck" + "github.com/quinntas/go-rest-template/internal/api/web" +) + +func NewV1Router(path string, ctx context.Context) { + router := web.NewHttpRouter(path, ctx) + + web.Route(router, http.MethodGet, "/health", ctx, healthcheckUseCase.Execute) +} diff --git a/api/shared/useCases/healthCheck/healthCheck.go b/api/shared/useCases/healthCheck/healthCheck.go new file mode 100644 index 0000000..cba8b1e --- /dev/null +++ b/api/shared/useCases/healthCheck/healthCheck.go @@ -0,0 +1,16 @@ +package healthcheckUseCase + +import ( + "context" + "net/http" + + "github.com/quinntas/go-rest-template/internal/api/utils" + "github.com/quinntas/go-rest-template/internal/api/web" +) + +func Execute(request *http.Request, response http.ResponseWriter, ctx context.Context) error { + web.JsonResponse(response, http.StatusOK, &utils.Map[string]{ + "message": "ok", + }) + return nil +} diff --git a/cmd/main.go b/cmd/main.go index 9ca6bb0..a4fe227 100644 --- a/cmd/main.go +++ b/cmd/main.go @@ -3,26 +3,20 @@ package main import ( "context" "fmt" - "log" - "net/http" + sharedRouter "github.com/quinntas/go-rest-template/api/shared/infra/router" "github.com/quinntas/go-rest-template/internal/api/utils/env" "github.com/quinntas/go-rest-template/internal/api/web" ) -func execute(request *http.Request, response http.ResponseWriter, ctx context.Context) error { - web.JsonResponse(response, http.StatusOK, &map[string]string{ - "message": "ok", - }) - return nil -} - func main() { envVariables := env.NewEnvVariables() - fmt.Println("[Server] running on", envVariables.Port) + ctx := context.WithValue(context.Background(), env.ENV_CTX_KEY, envVariables) + + sharedRouter.InitRouters(ctx) - web.Route(http.MethodGet, "/", execute) + fmt.Println("[Server] running on", fmt.Sprintf("http://localhost:%s", envVariables.Port)) - log.Fatal(http.ListenAndServe(fmt.Sprintf(":%s", envVariables.Port), web.ServerHandler(http.DefaultServeMux))) + web.Serve(envVariables.Port) } diff --git a/internal/api/utils/context.go b/internal/api/utils/context.go new file mode 100644 index 0000000..5d6f840 --- /dev/null +++ b/internal/api/utils/context.go @@ -0,0 +1,3 @@ +package utils + +type Key string diff --git a/internal/api/utils/env/constants.go b/internal/api/utils/env/constants.go new file mode 100644 index 0000000..2e1c614 --- /dev/null +++ b/internal/api/utils/env/constants.go @@ -0,0 +1,7 @@ +package env + +import "github.com/quinntas/go-rest-template/internal/api/utils" + +const ( + ENV_CTX_KEY utils.Key = "ENV" +) diff --git a/internal/api/utils/env/env.go b/internal/api/utils/env/env.go index 247dbc9..7931e5d 100644 --- a/internal/api/utils/env/env.go +++ b/internal/api/utils/env/env.go @@ -7,10 +7,9 @@ import ( ) type EnvVariables struct { - Port string + Port string } - func getEnv(key string, required bool) string { value := os.Getenv(key) if required && value == "" { @@ -19,13 +18,13 @@ func getEnv(key string, required bool) string { return value } -func NewEnvVariables() EnvVariables{ +func NewEnvVariables() EnvVariables { err := godotenv.Load() if err != nil { panic(err) } return EnvVariables{ - Port: getEnv("PORT", true), + Port: getEnv("PORT", true), } } diff --git a/internal/api/utils/types.go b/internal/api/utils/types.go new file mode 100644 index 0000000..6dc0513 --- /dev/null +++ b/internal/api/utils/types.go @@ -0,0 +1,3 @@ +package utils + +type Map[T interface{}] map[string]T diff --git a/internal/api/web/constants.go b/internal/api/web/constants.go index 8ed6cea..c46dca7 100644 --- a/internal/api/web/constants.go +++ b/internal/api/web/constants.go @@ -1,7 +1,21 @@ package web +import ( + "net/http" + + "github.com/quinntas/go-rest-template/internal/api/utils" +) + const ( - JSON_CTX_KEY = "JSON" - QUERY_CTX_KEY = "QUERY" - PATH_CTX_KEY = "PATH" + JSON_CTX_KEY utils.Key = "JSON" + QUERY_CTX_KEY utils.Key = "QUERY" + PATH_CTX_KEY utils.Key = "PATH" ) + +func applyDefaultHeaders(w http.ResponseWriter) { + w.Header().Set("Access-Control-Allow-Origin", "*") + w.Header().Set("Access-Control-Allow-Methods", "GET, POST, PATCH, DELETE, OPTIONS") + w.Header().Set("Access-Control-Allow-Headers", "Content-Type, Authorization") + w.Header().Set("Server", "Encom-Backend/1.0.0 (Golang)") + w.Header().Set("X-Powered-By", "Encom") +} diff --git a/internal/api/web/errors.go b/internal/api/web/errors.go index d4b5f18..b7165d3 100644 --- a/internal/api/web/errors.go +++ b/internal/api/web/errors.go @@ -1,11 +1,15 @@ package web -import "net/http" +import ( + "net/http" + + "github.com/quinntas/go-rest-template/internal/api/utils" +) type HttpError struct { status int message string - body interface{} + body utils.Map[interface{}] } func (e *HttpError) Error() string { @@ -13,16 +17,15 @@ func (e *HttpError) Error() string { } func (e *HttpError) ToJsonResponse(response http.ResponseWriter) { + e.body["message"] = e.message JsonResponse( response, e.status, - &map[string]string{ - "message": e.message, - }, + &e.body, ) } -func NewHttpError[T interface{}](status int, message string, body T) *HttpError { +func NewHttpError(status int, message string, body utils.Map[interface{}]) *HttpError { return &HttpError{ status: status, body: body, @@ -30,11 +33,21 @@ func NewHttpError[T interface{}](status int, message string, body T) *HttpError } } +func UnprocessableEntity() *HttpError { + return NewHttpError( + http.StatusUnprocessableEntity, + "unprocessable entity", + utils.Map[interface{}]{ + "hint": "please check the request's body", + }, + ) +} + func BadRequest() *HttpError { return NewHttpError( http.StatusBadRequest, "bad request", - map[string]string{}, + utils.Map[interface{}]{}, ) } @@ -42,6 +55,6 @@ func NotFound() *HttpError { return NewHttpError( http.StatusNotFound, "not found", - map[string]string{}, + utils.Map[interface{}]{}, ) } diff --git a/internal/api/web/handler.go b/internal/api/web/handler.go index 91bf9b5..4c3bbda 100644 --- a/internal/api/web/handler.go +++ b/internal/api/web/handler.go @@ -3,6 +3,7 @@ package web import ( "context" "encoding/json" + "fmt" "net/http" "net/url" ) @@ -32,7 +33,7 @@ func handleError(err error, response http.ResponseWriter) { } } -func httpHandler(ctx context.Context, useCase UseCase) http.HandlerFunc { +func HttpHandler(ctx context.Context, useCase UseCase) http.HandlerFunc { return func(response http.ResponseWriter, request *http.Request) { err := parseForm(request) if err != nil { @@ -55,7 +56,7 @@ func httpHandler(ctx context.Context, useCase UseCase) http.HandlerFunc { var data interface{} err := json.NewDecoder(request.Body).Decode(&data) if err != nil { - handleError(BadRequest(), response) + handleError(UnprocessableEntity(), response) return } ctx = context.WithValue(ctx, JSON_CTX_KEY, data) @@ -69,8 +70,13 @@ func httpHandler(ctx context.Context, useCase UseCase) http.HandlerFunc { } } -func Route(method string, path string, useCase UseCase) { - ctx := context.WithValue(context.Background(), PATH_CTX_KEY, path) +func notFoundHandler() { + http.HandleFunc("/", func(rw http.ResponseWriter, r *http.Request) { + handleError(NotFound(), rw) + }) +} - http.HandleFunc(method+" "+path, httpHandler(ctx, useCase)) +func Route(router HttpRouter, method string, path string, ctx context.Context, useCase UseCase) { + ctx = context.WithValue(ctx, PATH_CTX_KEY, path) + http.HandleFunc(fmt.Sprintf("%s %s%s", method, router.path, path), HttpHandler(ctx, useCase)) } diff --git a/internal/api/web/logger.go b/internal/api/web/logger.go index 45a131b..ce23e11 100644 --- a/internal/api/web/logger.go +++ b/internal/api/web/logger.go @@ -14,68 +14,63 @@ type statusWriter struct { status int } -func PrintResponseTime(responseTime time.Duration) string { - var str string +func printResponseTime(responseTime time.Duration) string { + var colour string if responseTime.Seconds() > 1 { - str = terminal.Red + colour = terminal.Red } else if responseTime.Seconds() > 0.5 { - str = terminal.Yellow + colour = terminal.Yellow } else { - str = terminal.Green + colour = terminal.Green } - return str + responseTime.String() + terminal.Reset + return colour + responseTime.String() + terminal.Reset } -func PrintStatus(status int) string { - var str string +func printStatus(status int) string { + var colour string switch { case status >= 200 && status < 300: - str = terminal.Green + colour = terminal.Green case status >= 300 && status < 400: - str = terminal.Cyan + colour = terminal.Cyan case status >= 400: - str = terminal.Red + colour = terminal.Red default: - str = terminal.Reset + colour = terminal.Reset } - return str + strconv.Itoa(status) + terminal.Reset + return colour + strconv.Itoa(status) + terminal.Reset } -func PrintMethod(method string) string { - var str string +func printMethod(method string) string { + var colour string switch method { case "GET": - str = terminal.Green + colour = terminal.Green case "POST": - str = terminal.Yellow + colour = terminal.Yellow case "PATCH": - str = terminal.Cyan + colour = terminal.Cyan case "DELETE": - str = terminal.Red + colour = terminal.Red default: - str = terminal.Reset + colour = terminal.Reset } - return str + method + terminal.Reset + return colour + method + terminal.Reset } -func (w *statusWriter) WriteHeader(status int) { +func (w *statusWriter) writeHeader(status int) { w.status = status w.ResponseWriter.WriteHeader(status) } -func ServerHandler(handler http.Handler) http.Handler { - return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { +func loggerHandler(handler http.Handler) http.Handler { + return http.HandlerFunc(func(rw http.ResponseWriter, r *http.Request) { start := time.Now() - w.Header().Set("Access-Control-Allow-Origin", "*") - w.Header().Set("Access-Control-Allow-Methods", "GET, POST, PATCH, DELETE, OPTIONS") - w.Header().Set("Access-Control-Allow-Headers", "Content-Type, Authorization") - w.Header().Set("Server", "Encom-Backend/1.0.0 (Golang)") - w.Header().Set("X-Powered-By", "Encom") - w.Header().Set("Cache-Control", "no-store, no-cache, must-revalidate, post-check=0, pre-check=0") - sw := &statusWriter{ResponseWriter: w} + applyDefaultHeaders(rw) + sw := &statusWriter{ResponseWriter: rw} handler.ServeHTTP(sw, r) end := time.Now() responseTime := end.Sub(start) - fmt.Println(r.RemoteAddr, PrintMethod(r.Method), r.URL, PrintResponseTime(responseTime), PrintStatus(sw.status)) + fmt.Println(r.RemoteAddr, printMethod(r.Method), r.URL, printResponseTime(responseTime), printStatus(sw.status)) }) } diff --git a/internal/api/web/responses.go b/internal/api/web/responses.go index 283e63e..16f56be 100644 --- a/internal/api/web/responses.go +++ b/internal/api/web/responses.go @@ -3,9 +3,11 @@ package web import ( "encoding/json" "net/http" + + "github.com/quinntas/go-rest-template/internal/api/utils" ) -func JsonResponse[T interface{}](response http.ResponseWriter, status int, data *T) { +func JsonResponse[T interface{}](response http.ResponseWriter, status int, data *utils.Map[T]) { response.Header().Set("Content-Type", "application/json") response.WriteHeader(status) encoder := json.NewEncoder(response) diff --git a/internal/api/web/router.go b/internal/api/web/router.go new file mode 100644 index 0000000..2c9cdc5 --- /dev/null +++ b/internal/api/web/router.go @@ -0,0 +1,17 @@ +package web + +import "context" + +type HttpRouter struct { + path string + ctx context.Context +} + +func NewHttpRouter(path string, ctx context.Context) HttpRouter { + return HttpRouter{ + path: path, + ctx: ctx, + } +} + +type Router func(path string, ctx context.Context) diff --git a/internal/api/web/server.go b/internal/api/web/server.go new file mode 100644 index 0000000..028fa59 --- /dev/null +++ b/internal/api/web/server.go @@ -0,0 +1,17 @@ +package web + +import ( + "fmt" + "net/http" +) + +func Serve(port string) { + addr := fmt.Sprintf(":%s", port) + + notFoundHandler() + + err := http.ListenAndServe(addr, loggerHandler(http.DefaultServeMux)) + if err != nil { + panic(err) + } +}