Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
112 changes: 98 additions & 14 deletions README.MD
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,25 @@ 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)
3. [Register Controllers in main.go](#3-register-controllers-in-maingo)
- [🧱 Core Concepts](#-core-concepts)
- [💡 Why These Matter](#-why-these-matter)
- [🧪 Response Builder](#-response-builder)
- [📌 Example JSON Response](#-example-json-response)
- [🌐 CORS Middleware](#-cors-middleware)
- [✅ Registering the CORS Middleware](#-registering-the-cors-middleware)
- [⚙️ Behavior](#-behavior)
- [🛡 Example: Block all but GET](#-example-block-all-but-get)
- [❤️ Inspired By](#-inspired-by)

---

## 🚀 Usage

### 1. Define Request/Response DTOs
Expand All @@ -39,11 +58,11 @@ type UserResponse struct {
type UsersController struct{}

func (c *UsersController) BasePath() string {
return "/users"
return "/api/v1/users"
}

func (c *UsersController) Routes() []core.RouteEntry {
return []core.RouteEntry{
func (c *UsersController) Routes() []types.Route {
return []types.Route{
{Method: "GET", Path: "/", Handler: "GetAll"},
{Method: "GET", Path: "/{userid}", Handler: "Get"},
{Method: "POST", Path: "/", Handler: "Post"},
Expand Down Expand Up @@ -85,18 +104,37 @@ func main() {

---

### 🧱 Core Concepts
## 🧱 Core Concepts
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. |
| **`ResponseEntity`** | A fluent builder for setting status, body, and headers. Example: `ResponseEntity.Status(200).Body(data).Send(w)` or return it directly from controllers. |
| **`HttpStatus`** | Enum-style constants for all HTTP status codes, e.g., `HttpStatus.OK`, `HttpStatus.CREATED`, etc., making your response code more readable. |
| **`HttpMethod`** | Enum-like constants for HTTP methods (`GET`, `POST`, etc.) and helpers like `IsValid(method)` to validate custom usage. |
| **`exception`** | Standardized error response utilities like `BadRequestException(...)` or `InternalServerException(...)` that send JSON error responses with status codes. |
| **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. |

### 💡 Why These Matter

- ✅ **Minimal and clean:** Core concepts like `Controller` and `Route` are simple, composable structs.

- ✅ **Extensible:** The middleware system uses generics and fluent chaining to support per-middleware config and lifecycle hooks.

| Concept | Description |
| ----------------- | --------------------------------------------------------------- |
| `Controller` | A struct implementing `BasePath()` and `Routes()` |
| `RouteEntry` | Defines each HTTP method, subpath, and method handler name |
| DTOs | Define `UserRequest`, `UserResponse`, etc. in `Models/` package |
| `BindArguments()` | Automatically binds path vars and JSON body to method arguments |
| `ResponseEntity` | Fluent builder for response body, status, and headers |
- ✅ **Type-safe binding:** Reflect-based argument resolution injects only what your handler expects — nothing more.

- ✅ **Production-ready responses:** Use `ResponseEntity` and `exception` to consistently shape output without boilerplate.

### 🧪 Response Builder Examples
- ✅ **Testable architecture:** Middleware and controllers can be unit tested with standard Go tools (`httptest`).

---

## 🧪 Response Builder
Example of just sending a message:
```go
ResponseEntity.Status(HttpStatus.OK).
Expand All @@ -123,10 +161,56 @@ Content-Type: application/json
X-Custom: example

{
"message": "User created",
"id": 1
"Id": 1
"Name": "Test"
"Email": "Test@example.com"
}
```
---
## 🌐 CORS Middleware

GoWeb includes a built-in CORS middleware that allows you to control which origins, methods, and headers are allowed to access your server across different domains. This is especially useful when building frontend-backend systems or public APIs.

### ✅ Registering the CORS Middleware

To enable it globally:
```go
app.Use(middlewares.CORS)
```

Then configure it as needed:
```go
middlewares.CORS.Config.AllowedOrigins = []string{"https://example.com"}
middlewares.CORS.Config.AllowedMethods = []string{"GET", "POST"}
middlewares.CORS.Config.AllowedHeaders = []string{"Content-Type", "Authorization"}
middlewares.CORS.Config.AllowCredentials = true
```
### ⚙️ Behavior
| Feature | Description |
| ------------------------- | ----------------------------------------------------------------------- |
| `AllowedOrigins` | List of allowed domains (use `"*"` for all) |
| `AllowedMethods` | List of allowed HTTP methods (`GET`, `POST`, etc.) |
| `AllowedHeaders` | List of allowed request headers |
| `AllowCredentials` | Enables `Access-Control-Allow-Credentials: true` |
| Auto-Handles `OPTIONS` | Returns `204 No Content` and skips route logic |
| Blocks Disallowed Methods | Returns `405 Method Not Allowed` if the request method is not permitted |

### 🛡 Example: Block all but GET

```go
app.Use(middlewares.CORS)
middlewares.CORS.Config.AllowedMethods = []string{"GET"}
```

If a client sends a `POST` request, the server will respond with:

```http request
HTTP/1.1 405 Method Not Allowed
Access-Control-Allow-Methods: GET
Access-Control-Allow-Origin: https://example.com
```

---

### ❤️ Inspired By

Expand Down
82 changes: 50 additions & 32 deletions app/internal/router.go
Original file line number Diff line number Diff line change
Expand Up @@ -56,15 +56,18 @@ func ListenImpl(routes []CompiledRoute, addr string) error {
}

func Dispatch(routes []CompiledRoute, w http.ResponseWriter, req *http.Request) {
normalizedPath := normalizePath(req.URL.Path)

for _, route := range routes {
if req.Method != route.Method {
continue
}
normalizedPath := normalizePath(req.URL.Path)
matches := route.Regex.FindStringSubmatch(normalizedPath)
if matches == nil {
continue
}

if req.Method != route.Method && req.Method != http.MethodOptions {
continue
}

pathVars := extractPathVars(route.ParamNames, matches[1:])
paramTypes := getParamTypes(route.Handler.Type())
argNames := buildArgNames(paramTypes, route.ParamNames)
Expand All @@ -73,38 +76,42 @@ func Dispatch(routes []CompiledRoute, w http.ResponseWriter, req *http.Request)
return
}

// Controller-level pre/post middleware
var PreMiddlewares []types.MiddlewareFunc
if ctrl, ok := route.CtrlValue.Interface().(interface{ PreMiddleware() []types.MiddlewareFunc }); ok {
PreMiddlewares = ctrl.PreMiddleware()
// --- Controller-level middleware
var ctrlPre []types.Middleware
if ctrl, ok := route.CtrlValue.Interface().(interface{ PreMiddleware() []types.Middleware }); ok {
ctrlPre = ctrl.PreMiddleware()
}

var PostMiddlewares []types.MiddlewareFunc
if ctrl, ok := route.CtrlValue.Interface().(interface{ PostMiddleware() []types.MiddlewareFunc }); ok {
PostMiddlewares = ctrl.PostMiddleware()
var ctrlPost []types.Middleware
if ctrl, ok := route.CtrlValue.Interface().(interface{ PostMiddleware() []types.Middleware }); ok {
ctrlPost = ctrl.PostMiddleware()
}

// Build the middleware chain
chain := make([]types.MiddlewareFunc, 0, len(types.PreMiddlewares)+len(PreMiddlewares)+1+len(PostMiddlewares)+len(types.PostMiddlewares))
chain = append(chain, types.PreMiddlewares...)
chain = append(chain, PreMiddlewares...)
chain = append(chain, func(ctx *types.MiddlewareContext) error {
result := route.Handler.Call(args)
if len(result) != 1 {
exception.InternalServerException("Expected 1 return value").Send(w)
return nil
}
resp, ok := result[0].Interface().(*types.ResponseEntity)
if ok {
ctx.ResponseEntity = resp
}
return ctx.Next()
})
// --- Build the chain
chain := make([]types.MiddlewareFunc, 0,
len(types.PreMiddlewares)+len(ctrlPre)+1+len(ctrlPost)+len(types.PostMiddlewares),
)

chain = append(chain, types.ConvertMiddewaresToFuncs(types.PreMiddlewares)...)
chain = append(chain, types.ConvertMiddewaresToFuncs(ctrlPre)...)

if req.Method != http.MethodOptions {
chain = append(chain, func(ctx *types.MiddlewareContext) error {
result := route.Handler.Call(args)
if len(result) != 1 {
exception.InternalServerException("Expected 1 return value").Send(w)
return nil
}
if resp, ok := result[0].Interface().(*types.ResponseEntity); ok {
ctx.ResponseEntity = resp
}
return ctx.Next()
})
}

chain = append(chain, PostMiddlewares...)
chain = append(chain, types.PostMiddlewares...)
chain = append(chain, types.ConvertMiddewaresToFuncs(ctrlPost)...)
chain = append(chain, types.ConvertMiddewaresToFuncs(types.PostMiddlewares)...)

// Create the middleware context
mwCtx := &types.MiddlewareContext{
Request: req,
ResponseWriter: w,
Expand All @@ -115,14 +122,25 @@ func Dispatch(routes []CompiledRoute, w http.ResponseWriter, req *http.Request)

_ = mwCtx.Next()

// Serve the response if set
if mwCtx.ResponseEntity != nil {
mwCtx.ResponseEntity.Send(w)
}
return
}

// Not found
if req.Method == http.MethodOptions {
mwCtx := &types.MiddlewareContext{
Request: req,
ResponseWriter: w,
ResponseEntity: nil,
Index: -1,
Chain: types.ConvertMiddewaresToFuncs(types.PreMiddlewares),
}
_ = mwCtx.Next()
return
}

// Fallback
exception.NotFoundException("Route not found").Send(w)
}

Expand Down
16 changes: 11 additions & 5 deletions app/middleware.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,15 +2,21 @@ package app

import "github.com/isaacwallace123/GoWeb/app/types"

// Register pre-middleware
func Use(mw ...types.MiddlewareFunc) {
// Register pre-middleware (as Middleware interface, not just funcs)
func Use(mw ...types.Middleware) {
types.PreMiddlewares = append(types.PreMiddlewares, mw...)
}

// Register post-middleware
func UseAfter(mw ...types.MiddlewareFunc) {
func UseAfter(mw ...types.Middleware) {
types.PostMiddlewares = append(types.PostMiddlewares, mw...)
}

func Pre() []types.MiddlewareFunc { return types.PreMiddlewares }
func Post() []types.MiddlewareFunc { return types.PostMiddlewares }
// Optional accessors
func Pre() []types.MiddlewareFunc {
return types.ConvertMiddewaresToFuncs(types.PreMiddlewares)
}

func Post() []types.MiddlewareFunc {
return types.ConvertMiddewaresToFuncs(types.PostMiddlewares)
}
37 changes: 28 additions & 9 deletions app/middleware_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,41 +5,60 @@ import (
"testing"
)

// Clears global middleware slices before each test for isolation.
func clearMiddleware() {
types.PreMiddlewares = nil
types.PostMiddlewares = nil
}

func dummyPre(ctx *types.MiddlewareContext) error { return nil }
func dummyPost(ctx *types.MiddlewareContext) error { return nil }
// Dummy middleware implementation
type dummyMiddleware struct {
name string
}

func (d *dummyMiddleware) Func() types.MiddlewareFunc {
return func(ctx *types.MiddlewareContext) error {
return nil
}
}

func TestUseRegistersPreMiddleware(t *testing.T) {
clearMiddleware()
Use(dummyPre)
d := &dummyMiddleware{name: "pre"}
Use(d)

if len(types.PreMiddlewares) != 1 {
t.Errorf("expected 1 pre-middleware, got %d", len(types.PreMiddlewares))
}
if Pre()[0] == nil {

fn := Pre()[0]
if fn == nil {
t.Error("Pre() did not return a valid middleware func")
}
}

func TestUseAfterRegistersPostMiddleware(t *testing.T) {
clearMiddleware()
UseAfter(dummyPost)
d := &dummyMiddleware{name: "post"}
UseAfter(d)

if len(types.PostMiddlewares) != 1 {
t.Errorf("expected 1 post-middleware, got %d", len(types.PostMiddlewares))
}
if Post()[0] == nil {

fn := Post()[0]
if fn == nil {
t.Error("Post() did not return a valid middleware func")
}
}

func TestMultipleMiddlewares(t *testing.T) {
clearMiddleware()
Use(dummyPre, dummyPre)
UseAfter(dummyPost, dummyPost)
d1 := &dummyMiddleware{name: "a"}
d2 := &dummyMiddleware{name: "b"}

Use(d1, d2)
UseAfter(d1, d2)

if len(types.PreMiddlewares) != 2 {
t.Errorf("expected 2 pre-middlewares, got %d", len(types.PreMiddlewares))
}
Expand Down
Loading