From f8b93af5c9c967022fa175f86b2a137127d7f57e Mon Sep 17 00:00:00 2001 From: Drew Bednar Date: Wed, 22 Jan 2025 13:50:07 -0500 Subject: [PATCH] saving the work --- .gitignore | 1 + README.md | 2 +- cmd/appd/main.go | 44 +++++++++++++++++ go.mod | 3 ++ internal/config.go | 12 +++++ internal/handlers.go | 34 +++++++++++++ internal/logging.go | 35 ++++++++++++++ internal/models.go | 54 +++++++++++++++++++++ internal/routes.go | 20 ++++++++ internal/server.go | 32 +++++++++++++ internal/util.go | 110 +++++++++++++++++++++++++++++++++++++++++++ 11 files changed, 346 insertions(+), 1 deletion(-) create mode 100644 cmd/appd/main.go create mode 100644 go.mod create mode 100644 internal/config.go create mode 100644 internal/handlers.go create mode 100644 internal/logging.go create mode 100644 internal/models.go create mode 100644 internal/routes.go create mode 100644 internal/server.go create mode 100644 internal/util.go diff --git a/.gitignore b/.gitignore index adf8f72..26ef082 100644 --- a/.gitignore +++ b/.gitignore @@ -21,3 +21,4 @@ # Go workspace file go.work +tmp/ \ No newline at end of file diff --git a/README.md b/README.md index 259c552..b4938ac 100644 --- a/README.md +++ b/README.md @@ -1,3 +1,3 @@ # cookiecutter-golang-server -My cookiecutter project layout for a golang server application \ No newline at end of file +My cookiecutter project layout for a golang server application. This is delivered as a git template. \ No newline at end of file diff --git a/cmd/appd/main.go b/cmd/appd/main.go new file mode 100644 index 0000000..9c088ce --- /dev/null +++ b/cmd/appd/main.go @@ -0,0 +1,44 @@ +package main + +import ( + "context" + "fmt" + "io" + "os" + "os/signal" +) + +// run serves as the "entrypoint" of the application +// +// Operating system fundamentals are passed into run as arguments. +// This makes your programs much easier to test because test code can +// call run to execute your program, controlling arguments, and all +// streams, just by passing different arguments. +// +// Value Type Description +// os.Args []string The arguments passed in when executing your program. It’s also used for parsing flags. +// os.Stdin io.Reader For reading input +// os.Stdout io.Writer For writing output +// os.Stderr io.Writer For writing error and logs +// os.Getenv func(string) string For reading environment variables +// os.Getwd func() (string, error) Get the working directory +// +// If you keep away from any global scope data, you can usually use t.Parallel() in more places, +// to speed up your test suites. Everything is self-contained, so multiple calls to run don’t +// interfere with each other. +func run(ctx context.Context, w io.Writer, args []string) error { + ctx, cancel := signal.NotifyContext(ctx, os.Interrupt) + defer cancel() + + logger := InitLoggging() + + return nil +} + +func main() { + ctx := context.Background() + if err := run(ctx, os.Stdout, os.Args); err != nil { + fmt.Fprintf(os.Stderr, "%s\n", err) + os.Exit(1) + } +} diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..d151f66 --- /dev/null +++ b/go.mod @@ -0,0 +1,3 @@ +module git.runcible.io/androiddrew/cookiecutter-golang-server + +go 1.23.3 diff --git a/internal/config.go b/internal/config.go new file mode 100644 index 0000000..9b82b69 --- /dev/null +++ b/internal/config.go @@ -0,0 +1,12 @@ +package internal + +type Config struct { + msg string +} + +func (c *Config) GetMessage() string { + if c.msg == "" { + return "dirp" + } + return c.msg +} diff --git a/internal/handlers.go b/internal/handlers.go new file mode 100644 index 0000000..5dd7482 --- /dev/null +++ b/internal/handlers.go @@ -0,0 +1,34 @@ +package internal + +import ( + "log/slog" + "net/http" +) + +// handleSomething handles one of those web requests +// that you hear so much about. +func handleSomething(logger *slog.Logger, config *Config) http.Handler { + // provides a closure environment for the function + // thing := prepareThing() + msg := config.GetMessage() + return http.HandlerFunc( + func(w http.ResponseWriter, r *http.Request) { + // use thing to handle request + logger.Info("Handle something called", "msg", msg) + w.Write([]byte("handledSomething...")) + }, + ) +} + +// handleHealthzPlease returns a healthyu en +func handleHealthzPlease(logger *slog.Logger) http.Handler { + response := map[string]string{"status": "healthy"} + return http.HandlerFunc( + func(w http.ResponseWriter, r *http.Request) { + logger.Debug("Health endpoint called", "method", r.Method, "url", r.URL.Path) + + if err := encode(w, r, http.StatusOK, response); err != nil { + serverError(logger, w, r, err) + } + }) +} diff --git a/internal/logging.go b/internal/logging.go new file mode 100644 index 0000000..c7fe98b --- /dev/null +++ b/internal/logging.go @@ -0,0 +1,35 @@ +package internal + +import ( + "io" + "log/slog" + "strings" +) + +func parseLogLevel(levelStr string) slog.Level { + switch strings.ToUpper(levelStr) { + case "DEBUG": + return slog.LevelDebug + case "INFO": + return slog.LevelInfo + case "WARN": + return slog.LevelWarn + case "ERROR": + return slog.LevelError + default: + return slog.LevelInfo // Default level + } +} + +// InitLoggging initializes global structured logging for the entire application +// +// Stderr is used for diagnostics and logging. Stdout is used for program +// output. Stderr should be used since it will have greater likely hood of being +// seen if a programs output is being redirected. +func InitLogging(level string, w io.Writer) *slog.Logger { + parsedLogLevel := parseLogLevel(level) + loggerHandler := slog.NewJSONHandler(w, &slog.HandlerOptions{Level: parsedLogLevel, AddSource: true}) + logger := slog.New(loggerHandler) + slog.SetDefault(logger) + return logger +} diff --git a/internal/models.go b/internal/models.go new file mode 100644 index 0000000..ae3f162 --- /dev/null +++ b/internal/models.go @@ -0,0 +1,54 @@ +package internal + +import ( + "context" + "database/sql" + "time" +) + +type ExampleModel struct { + ID int + CreatedAt time.Time + UpdatedAt time.Time + Title sql.NullString + Content sql.NullString +} + +type ExampleModelService struct { + DB *sql.DB +} + +func (s *ExampleModelService) CreateExample(ctx context.Context, example *ExampleModel) (int, error) { + tx, err := s.DB.BeginTx(ctx, nil) + + if err != nil { + return 0, err + } + defer tx.Rollback() + + id, err := createExample(ctx, tx, example) + if err != nil { + return id, err + } + + return id, tx.Commit() +} + +func createExample(ctx context.Context, tx *sql.Tx, example *ExampleModel) (int, error) { + // assign current user to model + // userID := UserIDFromContext(ctx) + // if userID == 0 { + // return 0, wtf.Errorf(wtf.EUNAUTHORIZED, "You must be logged in to create a dial.") + // } + // example.UserID = wtf.UserIDFromContext(ctx) + stmt := "INSERT INTO example (title, content) VALUES (?, ?)" + result, err := tx.ExecContext(ctx, stmt, example.Title, example.Content) + if err != nil { + return 0, err + } + id, err := result.LastInsertId() + if err != nil { + return 0, err + } + return int(id), nil +} diff --git a/internal/routes.go b/internal/routes.go new file mode 100644 index 0000000..65bb5a0 --- /dev/null +++ b/internal/routes.go @@ -0,0 +1,20 @@ +package internal + +import ( + "log/slog" + "net/http" +) + +func addRoutes(mux *http.ServeMux, + logger *slog.Logger, + config *Config, + //tenantStore *TenantStore + +) http.Handler { + // mux.Handle("/api/v1/", handleTenantsGet(logger, tenantsStore)) + // mux.Handle("/oauth2/", handleOAuth2Proxy(logger, authProxy)) + mux.Handle("/something", handleSomething(logger, config)) + mux.Handle("/healthz", handleHealthzPlease(logger)) + mux.Handle("/", http.NotFoundHandler()) + return mux +} diff --git a/internal/server.go b/internal/server.go new file mode 100644 index 0000000..781fb9e --- /dev/null +++ b/internal/server.go @@ -0,0 +1,32 @@ +package internal + +import ( + "log/slog" + "net/http" +) + +// NewServer is responsible for all the top-level HTTP stuff that +// applies to all endpoints, like CORS, auth middleware, and logging. +// +// in tests `nil` can be passed to components that will not be strictly +// used. +func NewServer(logger *slog.Logger, + config *Config, + // commentStore *commentStore + // anotherStore *anotherStore +) http.Handler { + mux := http.NewServeMux() + addRoutes( + mux, + logger, + config, + // commentStore, + // anotherStore, + ) + var handler http.Handler = mux + // Add Middleware + // handler = someMiddleware(handler) + // handler = someMiddleware2(handler) + // handler = someMiddleware3(handler) + return handler +} diff --git a/internal/util.go b/internal/util.go new file mode 100644 index 0000000..7de9618 --- /dev/null +++ b/internal/util.go @@ -0,0 +1,110 @@ +package internal + +import ( + "encoding/json" + "fmt" + "log/slog" + "net/http" + "runtime/debug" +) + +// encode writes a JSON-encoded response to the provided http.ResponseWriter. +// +// Parameters: +// - w: The http.ResponseWriter where the response will be written. +// - r: The *http.Request associated with the response. (Unused in this function but could be relevant for context.) +// - status: The HTTP status code to send with the response. +// - v: The value to encode and send as the JSON response. Can be of any type. +// +// Returns: +// - An error if the JSON encoding or writing to the ResponseWriter fails, otherwise nil. +func encode[T any](w http.ResponseWriter, r *http.Request, status int, v T) error { + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(status) + if err := json.NewEncoder(w).Encode(v); err != nil { + return fmt.Errorf("encode json: %w", err) + } + return nil +} + +// decode reads and decodes a JSON-encoded request body into a value of the specified type. +// +// Parameters: +// - r: The *http.Request containing the JSON-encoded body to decode. +// +// Returns: +// - The decoded value of type T. +// - An error if decoding fails, or if the body contains invalid JSON. +func decode[T any](r *http.Request) (T, error) { + var v T + if err := json.NewDecoder(r.Body).Decode(&v); err != nil { + return v, fmt.Errorf("decode json: %w", err) + } + return v, nil +} + +// serverError logs an internal server error and sends a 500 Internal Server Error +// response to the client. +// +// Usage: +// This function is intended for use when handling unexpected server-side errors. +// It logs the error details along with the HTTP request method, URI, and a stack +// trace for debugging purposes. After logging, it sends a standardized 500 Internal +// Server Error response to the client. +// +// Parameters: +// - logger: The *slog.Logger instance used for logging error details. +// - w: The http.ResponseWriter to send the response to. +// - r: The *http.Request that triggered the error. Used to extract method and URI. +// - err: The error instance to log, providing context about the issue. +// +// Example: +// +// func handler(w http.ResponseWriter, r *http.Request) { +// err := someOperation() +// if err != nil { +// serverError(logger, w, r, err) +// return +// } +// // Handle request normally +// } +func serverError(logger *slog.Logger, w http.ResponseWriter, r *http.Request, err error) { + var ( + method = r.Method + uri = r.URL.RequestURI() + // Use debug.Stack() to get the stack trace. This returns a byte slice, which + // we need to convert to a string so that it's readable in the log entry. + trace = string(debug.Stack()) + ) + + logger.Error(err.Error(), "method", method, "uri", uri, "trace", trace) + http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError) +} + +// clientError sends an HTTP error response to the client with the specified +// status code and its corresponding description. +// +// Usage: +// This function is useful for handling HTTP errors in a consistent manner. +// It sends the appropriate HTTP status text as the response body, along with +// the given status code as the response status. This is typically used in web +// applications to return standardized error messages for various client-side +// errors. +// +// Parameters: +// - w: The http.ResponseWriter to send the response to. +// - status: The HTTP status code to return. It should be a valid HTTP status +// code as defined in the http package. +// +// Example: +// +// func handler(w http.ResponseWriter, r *http.Request) { +// if !isAuthorized(r) { +// clientError(w, http.StatusForbidden) // Responds with "403 Forbidden" +// return +// } +// // Handle request normally +// } +func clientError(w http.ResponseWriter, status int) { + http.Error(w, http.StatusText(status), status) +}