diff --git a/.gitignore b/.gitignore index 00a8c8b..74685be 100644 --- a/.gitignore +++ b/.gitignore @@ -2,4 +2,6 @@ dev/ main.go GoWeb -Makefile \ No newline at end of file +Makefile +public/ +static/ diff --git a/README.MD b/README.MD index c389c6a..f49074e 100644 --- a/README.MD +++ b/README.MD @@ -18,7 +18,6 @@ GoWeb is a lightweight Go web framework that mimics the structure, design, and f ## 📚 Table of Contents -- [✨ Features](#-features) - [🚀 Usage](#-usage) 1. [Define Request/Response DTOs](#1-define-requestresponse-dtos) 2. [Create a Controller](#2-create-a-controller) @@ -27,6 +26,10 @@ GoWeb is a lightweight Go web framework that mimics the structure, design, and f - [💡 Why These Matter](#-why-these-matter) - [🧪 Response Builder](#-response-builder) - [📌 Example JSON Response](#-example-json-response) +- [⚙️ Configuration](#-configuration) + - [Example Configs](#example-configs) + - [How It Works](#how-it-works) + - [Loading and Using Configs](#loading-and-using-configs) - [🌐 CORS Middleware](#-cors-middleware) - [✅ Registering the CORS Middleware](#-registering-the-cors-middleware) - [⚙️ Behavior](#-behavior) @@ -86,12 +89,6 @@ func main() { &controllers.UsersController{}, // You can create a controller through OOP controllers.New(), // You can create a controller using the builder ) - - // Register pre-middleware - app.Use(middlewares.LoggingPre) - - // Register post-middleware - app.UseAfter(middlewares.LoggingPost) // Launch the server logger.Info("Server listening on http://localhost:8080") @@ -108,7 +105,7 @@ func main() { GoWeb is built on a clean and extendable foundation inspired by Spring Boot, but optimized for Go. Below are the key architectural components of the framework: | Concept | Description | -| --------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +|-----------------------------|------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| | **`Controller`** | A struct implementing `BasePath()` and `Routes()` to define a route group. It may embed `ControllerBase` to enable optional controller-specific middleware. | | **`Route`** | Defines a single HTTP endpoint via `Method`, `Path`, and a string-based `Handler` name that maps to a function in the controller. | | **DTOs** | Plain structs representing request or response data (e.g., `UserRequest`, `UserResponse`). GoWeb automatically binds path/query/body values to arguments. | @@ -119,6 +116,7 @@ GoWeb is built on a clean and extendable foundation inspired by Spring Boot, but | **Middleware** | Middleware objects implement the `Middleware` interface. They're registered globally or per controller using `app.Use(...)` or `controller.Use(...)`. | | **Middleware Builder** | Use `NewMiddlewareBuilder(...)` to create strongly-typed, reusable middleware with config (`.Config`), init logic (`.WithInit()`), and error hooks (`.OnError()`). | | **Request Context Helpers** | Access path params, query strings, and headers using `types.PathVar(ctx, "id")`, `QueryParam(ctx, "q")`, and `Header(ctx, "X-Token")`. Injected automatically by the router. | +| **Configuration** | A very straight-forward and easy way to manage configurations that your entire project can easily access via the `app/config` util | ### 💡 Why These Matter @@ -166,6 +164,70 @@ X-Custom: example "Email": "Test@example.com" } ``` +--- + +## ⚙️ Configuration + +This project supports simple, extensible configuration using a single JSON file, typically located at `./application.json`. All key server settings—such as port and static resource mappings—are defined here. + +### Example Configs + +```json +{ + "server": { + "port": 8080 + }, + "static": [ + { "path": "/public", "directory": "./public" }, + { "path": "/static", "directory": "./static" } + ] +} +``` + +### How It Works +- **Server Port** + - The server listens on the port defined at server.port + - you can use any valid port number (e.g., `8080`, `3000`, etc.). +- **Static Resources** + - Each entry in the `static` array maps a URL prefix (`path`) to a directory on disk (`directory`). + - For example, requests to `/public/example.png` will serve the file located at `./public/example.png`. + +### Adding or Changing Static Resources +To add more static resources, just simply add new entries in the `static` array: +```json +"static": [ + { "path": "/assets", "directory": "./assets" }, + { "path": "/media", "directory": "./media" } +] +``` +Each mapping supports any valid URL prefix and directory path (relative to your project root) +### Loading and Using Configs +On startup, the server loads `application.json` and applies the configuration automatically. Config values are available anywhere in your Go code via the config package (works as a singleton). +```go +import ( + "github.com/isaacwallace123/GoWeb/app/config" + "github.com/isaacwallace123/GoUtils/logger" + "github.com/isaacwallace123/GoWeb/app" +) + +func main() { + // This is load your configs into a singleton struct. + if err := config.LoadConfig("./dev/application.json"); err != nil { + logger.Fatal("Failed to load config: %v", err) + } + + router := app.NewRouter() + + // UseStatic is a method that will automatically create a static resource URI to the designated directory + for _, s := range config.Static { + router.UseStatic(s.Path, s.Directory) + } + + // Get the configured port (as string) + port := config.PortString() // e.g., ":8080" +} +``` + --- ## 🌐 CORS Middleware @@ -211,7 +273,6 @@ Access-Control-Allow-Origin: https://example.com ``` --- - ### ❤️ Inspired By - Java Spring Boot diff --git a/app/config/config.go b/app/config/config.go new file mode 100644 index 0000000..f38764c --- /dev/null +++ b/app/config/config.go @@ -0,0 +1,41 @@ +package config + +import ( + "github.com/isaacwallace123/GoUtils/jsonutil" + "github.com/isaacwallace123/GoWeb/app/types" + "os" + "strconv" +) + +type ServerConfig struct { + Port int `json:"port"` +} + +var ( + Server ServerConfig + Static []types.StaticConfig +) + +type appConfig struct { + Server ServerConfig `json:"server"` + Static []types.StaticConfig `json:"static"` +} + +func LoadConfig(path string) error { + file, err := os.ReadFile(path) + if err != nil { + return err + } + var loaded appConfig + if err := jsonutil.FromBytes(file, &loaded); err != nil { + return err + } + Server = loaded.Server + Static = loaded.Static + return nil +} + +// PortString returns ":8080" (or whatever garbage port you configured) +func PortString() string { + return ":" + strconv.Itoa(Server.Port) +} diff --git a/app/internal/router.go b/app/internal/router.go index 0fff3d9..6e3e6e8 100644 --- a/app/internal/router.go +++ b/app/internal/router.go @@ -48,11 +48,8 @@ func RegisterControllersImpl(controllers ...types.Controller) []CompiledRoute { return compiled } -func ListenImpl(routes []CompiledRoute, addr string) error { - http.HandleFunc("/", func(w http.ResponseWriter, req *http.Request) { - Dispatch(routes, w, req) - }) - return http.ListenAndServe(addr, nil) +func ListenImpl(router http.Handler, addr string) error { + return http.ListenAndServe(addr, router) } func Dispatch(routes []CompiledRoute, w http.ResponseWriter, req *http.Request) { diff --git a/app/router.go b/app/router.go index a528e95..459986c 100644 --- a/app/router.go +++ b/app/router.go @@ -1,13 +1,16 @@ package app import ( + "github.com/isaacwallace123/GoUtils/logger" "github.com/isaacwallace123/GoWeb/app/internal" "github.com/isaacwallace123/GoWeb/app/types" "net/http" + "strings" ) type Router struct { - routes []internal.CompiledRoute + routes []internal.CompiledRoute + resources []func(http.ResponseWriter, *http.Request) bool } // NewRouter creates a new Router. @@ -21,11 +24,46 @@ func (r *Router) RegisterControllers(controllers ...types.Controller) { } // Listen starts the HTTP server. -func (r *Router) Listen(addr string) error { - return internal.ListenImpl(r.routes, addr) -} +func (r *Router) Listen(addr string) error { return internal.ListenImpl(r, addr) } -// ServeHTTP allows Router to implement http.Handler. +// ServeHTTP first tries static handlers, then dispatches dynamic routes. func (r *Router) ServeHTTP(w http.ResponseWriter, req *http.Request) { + for _, handler := range r.resources { + if handler(w, req) { + return + } + } + internal.Dispatch(r.routes, w, req) } + +// UseStatic registers a static file handler for the given URL prefix and directory. +func (r *Router) UseStatic(prefix, dir string) { + if !strings.HasPrefix(prefix, "/") { + prefix = "/" + prefix + } + if len(prefix) > 1 && strings.HasSuffix(prefix, "/") { + prefix = strings.TrimSuffix(prefix, "/") + } + + fs := http.FileServer(http.Dir(dir)) + handler := func(w http.ResponseWriter, req *http.Request) bool { + path := req.URL.Path + + if path == prefix { + http.Redirect(w, req, prefix+"/", http.StatusMovedPermanently) + logger.Info("[Static] Redirected: %s → %s/", path, prefix) + return true + } + + if strings.HasPrefix(path, prefix+"/") { + logger.Info("[Static] %s → %s (%s)", prefix, dir, path) + http.StripPrefix(prefix, fs).ServeHTTP(w, req) + return true + } + return false + } + + r.resources = append(r.resources, handler) + logger.Info("[Static] Registered: %-12s → %s", prefix, dir) +} diff --git a/app/types/static.go b/app/types/static.go new file mode 100644 index 0000000..b6776fc --- /dev/null +++ b/app/types/static.go @@ -0,0 +1,6 @@ +package types + +type StaticConfig struct { + Path string `json:"path"` + Directory string `json:"directory"` +} diff --git a/go.mod b/go.mod index 1fdc146..9b0a027 100644 --- a/go.mod +++ b/go.mod @@ -2,4 +2,4 @@ module github.com/isaacwallace123/GoWeb go 1.24.2 -require github.com/isaacwallace123/GoUtils v1.0.1 +require github.com/isaacwallace123/GoUtils v1.0.2 diff --git a/go.sum b/go.sum index b2cb67f..fb3611d 100644 --- a/go.sum +++ b/go.sum @@ -1,2 +1,2 @@ -github.com/isaacwallace123/GoUtils v1.0.1 h1:XGG8zKeG+K/mWf6NQw1KIOppiz9WyxHuGVvFuc4MWC0= -github.com/isaacwallace123/GoUtils v1.0.1/go.mod h1:7SX/JIf8Zdml2dh6Zn6vNSrEsYlXwlkLzU8o85UG3Rk= +github.com/isaacwallace123/GoUtils v1.0.2 h1:CrWqtcuuWU6DxNWSbf5bkOGiZsILQvnNasvADYfgle4= +github.com/isaacwallace123/GoUtils v1.0.2/go.mod h1:7SX/JIf8Zdml2dh6Zn6vNSrEsYlXwlkLzU8o85UG3Rk= diff --git a/pkg/middlewares/logging.go b/pkg/middlewares/logging.go index 6a623d4..6ff88db 100644 --- a/pkg/middlewares/logging.go +++ b/pkg/middlewares/logging.go @@ -27,7 +27,13 @@ var LoggingPost = types.NewMiddlewareBuilder("logging_post", &LoggingConfig{ }, func(ctx *types.MiddlewareContext, cfg *LoggingConfig) error { if cfg.Enabled && ctx.ResponseEntity != nil { methodColored := color.HTTPMethodToColor[ctx.Request.Method] + ctx.Request.Method + color.Reset - logger.Info("%s %s %d", methodColored, ctx.Request.URL.Path, ctx.ResponseEntity.StatusCode) + statusColor := color.HTTPStatusToColor(ctx.ResponseEntity.StatusCode) + + logger.Info("%s %s %s%d%s", + methodColored, + ctx.Request.URL.Path, + statusColor, ctx.ResponseEntity.StatusCode, color.Reset, + ) } return ctx.Next() })