From 8d52a57ed5fb0a87f5e609e8a5a1a4c8f878f474 Mon Sep 17 00:00:00 2001 From: hokamsingh Date: Wed, 21 Aug 2024 09:50:00 +0530 Subject: [PATCH] feat: http methods, body parser and improvements --- internal/core/context/context.go | 164 ++++++++++++++++++++++++ internal/core/middleware/json_parser.go | 33 ++++- internal/core/router/router.go | 164 ++++++++++++++++++++++-- pkg/lessgo/less.go | 2 + 4 files changed, 351 insertions(+), 12 deletions(-) create mode 100644 internal/core/context/context.go diff --git a/internal/core/context/context.go b/internal/core/context/context.go new file mode 100644 index 0000000..4eacc00 --- /dev/null +++ b/internal/core/context/context.go @@ -0,0 +1,164 @@ +package context + +import ( + "bytes" + "encoding/json" + "errors" + "io" + "net/http" +) + +// Context holds the request and response writer and provides utility methods. +type Context struct { + Req *http.Request + Res http.ResponseWriter +} + +// NewContext creates a new Context instance. +// +// This function initializes a new Context with the provided request and response writer. +// +// Example usage: +// +// ctx := context.NewContext(req, res) +func NewContext(req *http.Request, res http.ResponseWriter) *Context { + return &Context{Req: req, Res: res} +} + +// GetJSONBody retrieves the parsed JSON body from the request context. +func (c *Context) GetJSONBody() (map[string]interface{}, bool) { + key := "jsonBody" + jsonBody, ok := c.Req.Context().Value(key).(map[string]interface{}) + return jsonBody, ok +} + +// JSON sends a JSON response with the given status code. +// +// This method sets the Content-Type to application/json and writes the provided value as a JSON response. +// +// Parameters: +// +// status (int): The HTTP status code to send with the response. +// v (interface{}): The data to encode as JSON and send in the response. +// +// Example usage: +// +// ctx.JSON(http.StatusOK, map[string]string{"message": "success"}) +func (c *Context) JSON(status int, v interface{}) { + c.Res.Header().Set("Content-Type", "application/json") + c.Res.WriteHeader(status) + json.NewEncoder(c.Res).Encode(v) +} + +// Error sends an error response with the given status code and message. +// +// This method sets the Content-Type to application/json and writes an error message with the specified HTTP status code. +// +// Parameters: +// +// status (int): The HTTP status code to send with the response. +// message (string): The error message to include in the response. +// +// Example usage: +// +// ctx.Error(http.StatusBadRequest, "Invalid request") +func (c *Context) Error(status int, message string) { + c.Res.Header().Set("Content-Type", "application/json") + c.Res.WriteHeader(status) + json.NewEncoder(c.Res).Encode(map[string]string{"error": message}) +} + +// Body parses the JSON request body into the provided interface. +// +// This method decodes the JSON body of the request into the provided value. +// +// Parameters: +// +// v (interface{}): The value to decode the JSON into. +// +// Returns: +// +// error: An error if JSON decoding fails. +// +// Example usage: +// +// var data map[string]interface{} +// err := ctx.Body(&data) +func (c *Context) Body(v interface{}) error { + if c.Req.Body == nil { + return errors.New("request body is nil") + } + bodyBytes, err := io.ReadAll(c.Req.Body) + if err != nil { + return err + } + if len(bodyBytes) == 0 { + return errors.New("empty request body") + } + // Reset the body so it can be read again later if needed + c.Req.Body = io.NopCloser(bytes.NewBuffer(bodyBytes)) + return json.NewDecoder(bytes.NewReader(bodyBytes)).Decode(v) +} + +// Redirect sends a redirect response to the given URL. +// +// This method sends an HTTP redirect to the specified URL with the provided status code. +// +// Parameters: +// +// status (int): The HTTP status code for the redirect (e.g., http.StatusFound). +// url (string): The URL to redirect to. +// +// Example usage: +// +// ctx.Redirect(http.StatusSeeOther, "/new-url") +func (c *Context) Redirect(status int, url string) { + http.Redirect(c.Res, c.Req, url, status) +} + +// SetCookie adds a cookie to the response. +// +// This method sets a cookie with the given name, value, and options. +// +// Parameters: +// +// name (string): The name of the cookie. +// value (string): The value of the cookie. +// maxAge (int): The maximum age of the cookie in seconds. +// path (string): The path for which the cookie is valid. +// +// Example usage: +// +// ctx.SetCookie("session_id", "123456", 3600, "/") +func (c *Context) SetCookie(name, value string, maxAge int, path string) { + http.SetCookie(c.Res, &http.Cookie{ + Name: name, + Value: value, + MaxAge: maxAge, + Path: path, + HttpOnly: true, + }) +} + +// GetCookie retrieves a cookie value from the request. +// +// This method fetches the value of a cookie with the specified name from the request. +// +// Parameters: +// +// name (string): The name of the cookie to retrieve. +// +// Returns: +// +// (string, bool): The value of the cookie and a boolean indicating if the cookie was found. +// +// Example usage: +// +// value, ok := ctx.GetCookie("session_id") +func (c *Context) GetCookie(name string) (string, bool) { + cookie, err := c.Req.Cookie(name) + if err != nil { + return "", false + } + return cookie.Value, true +} diff --git a/internal/core/middleware/json_parser.go b/internal/core/middleware/json_parser.go index e55dfe6..f595306 100644 --- a/internal/core/middleware/json_parser.go +++ b/internal/core/middleware/json_parser.go @@ -1,19 +1,50 @@ package middleware import ( + "bytes" "context" "encoding/json" + "io" "net/http" ) +// OLD version +// func JSONParser(next http.Handler) http.Handler { +// return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { +// if r.Header.Get("Content-Type") == "application/json" { +// var body map[string]interface{} +// if err := json.NewDecoder(r.Body).Decode(&body); err != nil { +// http.Error(w, "Invalid JSON", http.StatusBadRequest) +// return +// } +// key := "jsonBody" +// r = r.WithContext(context.WithValue(r.Context(), key, body)) +// } +// next.ServeHTTP(w, r) +// }) +// } + func JSONParser(next http.Handler) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { if r.Header.Get("Content-Type") == "application/json" { + // Read the body into a byte slice + bodyBytes, err := io.ReadAll(r.Body) + if err != nil { + http.Error(w, "Invalid JSON", http.StatusBadRequest) + return + } + + // Restore the io.ReadCloser to its original state + r.Body = io.NopCloser(bytes.NewBuffer(bodyBytes)) + + // Decode the body into a map var body map[string]interface{} - if err := json.NewDecoder(r.Body).Decode(&body); err != nil { + if err := json.Unmarshal(bodyBytes, &body); err != nil { http.Error(w, "Invalid JSON", http.StatusBadRequest) return } + + // Store the parsed JSON in the context key := "jsonBody" r = r.WithContext(context.WithValue(r.Context(), key, body)) } diff --git a/internal/core/router/router.go b/internal/core/router/router.go index 23c7209..f95f907 100644 --- a/internal/core/router/router.go +++ b/internal/core/router/router.go @@ -9,14 +9,17 @@ import ( "time" "github.com/gorilla/mux" + "github.com/hokamsingh/lessgo/internal/core/context" "github.com/hokamsingh/lessgo/internal/core/middleware" ) +// Router represents an HTTP router with middleware support and error handling. type Router struct { Mux *mux.Router middleware []middleware.Middleware } +// Option is a function that configures a Router. type Option func(*Router) // Default CORS options @@ -26,7 +29,15 @@ var defaultCORSOptions = middleware.CORSOptions{ AllowedHeaders: []string{"Content-Type", "Authorization"}, } -// NewRouter creates a new Router with optional configuration +// NewRouter creates a new Router with optional configuration. +// You can pass options like WithCORS or WithJSONParser to configure the router. +// +// Example usage: +// +// r := router.NewRouter( +// router.WithCORS(middleware.CORSOptions{}), +// router.WithJSONParser(), +// ) func NewRouter(options ...Option) *Router { r := &Router{ Mux: mux.NewRouter(), @@ -40,7 +51,12 @@ func NewRouter(options ...Option) *Router { return r } -// SubRouter creates a subrouter with the given path prefix +// SubRouter creates a subrouter with the given path prefix. +// +// Example usage: +// +// subRouter := r.SubRouter("/api") +// subRouter.AddRoute("/ping", handler) func (r *Router) SubRouter(pathPrefix string) *Router { subRouter := &Router{ Mux: r.Mux.PathPrefix(pathPrefix).Subrouter(), @@ -49,7 +65,12 @@ func (r *Router) SubRouter(pathPrefix string) *Router { return subRouter } -// WithCORS enables CORS middleware with specific options +// WithCORS enables CORS middleware with specific options. +// This option configures the CORS settings for the router. +// +// Example usage: +// +// r := router.NewRouter(router.WithCORS(middleware.CORSOptions{...})) func WithCORS(options middleware.CORSOptions) Option { return func(r *Router) { corsMiddleware := middleware.NewCORSMiddleware(options) @@ -57,6 +78,12 @@ func WithCORS(options middleware.CORSOptions) Option { } } +// WithRateLimiter enables rate limiting middleware with the specified limit and interval. +// This option configures the rate limiter for the router. +// +// Example usage: +// +// r := router.NewRouter(router.WithRateLimiter(100, time.Minute)) func WithRateLimiter(limit int, interval time.Duration) Option { return func(r *Router) { rateLimiter := middleware.NewRateLimiter(limit, interval) @@ -64,6 +91,12 @@ func WithRateLimiter(limit int, interval time.Duration) Option { } } +// WithJSONParser enables JSON parsing middleware for request bodies. +// This option ensures that incoming JSON payloads are parsed and available in the request context. +// +// Example usage: +// +// r := router.NewRouter(router.WithJSONParser()) func WithJSONParser() Option { return func(r *Router) { jsonParser := middleware.MiddlewareWrapper{HandlerFunc: middleware.JSONParser} @@ -71,6 +104,12 @@ func WithJSONParser() Option { } } +// WithCookieParser enables cookie parsing middleware. +// This option ensures that cookies are parsed and available in the request context. +// +// Example usage: +// +// r := router.NewRouter(router.WithCookieParser()) func WithCookieParser() Option { return func(r *Router) { cookieParser := middleware.MiddlewareWrapper{HandlerFunc: middleware.CookieParser} @@ -78,6 +117,12 @@ func WithCookieParser() Option { } } +// WithFileUpload enables file upload middleware with the specified upload directory. +// This option configures the router to handle file uploads and save them to the given directory. +// +// Example usage: +// +// r := router.NewRouter(router.WithFileUpload("/uploads")) func WithFileUpload(uploadDir string) Option { return func(r *Router) { fileUploadMiddleware := middleware.NewFileUploadMiddleware(uploadDir) @@ -85,17 +130,41 @@ func WithFileUpload(uploadDir string) Option { } } +// Use adds a middleware to the router's middleware stack. +// +// Example usage: +// +// r.Use(middleware.LoggingMiddleware{}) func (r *Router) Use(m middleware.Middleware) { r.middleware = append(r.middleware, m) } -func (r *Router) AddRoute(path string, handler http.HandlerFunc) { - // Apply logging and error handling to the handler - handler = r.withErrorHandling(handler) - handler = r.withLogging(handler) - r.Mux.HandleFunc(path, handler) +// AddRoute adds a route with the given path and handler function. +// This method applies context, error handling, and logging to the handler. +// +// Example usage: +// +// r.AddRoute("/ping", func(ctx *context.Context) { +// ctx.JSON(http.StatusOK, map[string]string{"message": "pong"}) +// }) +func (r *Router) AddRoute(path string, handler CustomHandler) { + // Create an HTTP handler function that uses the custom context + handlerFunc := WrapCustomHandler(handler) + // Wrap the handler function with error handling and logging + handlerFunc = r.withErrorHandling(handlerFunc) + handlerFunc = r.withLogging(handlerFunc) + r.Mux.HandleFunc(path, handlerFunc) } +// Start starts the HTTP server on the specified address. +// It applies all middleware and listens for incoming requests. +// +// Example usage: +// +// err := r.Start(":8080") +// if err != nil { +// log.Fatalf("Server failed: %v", err) +// } func (r *Router) Start(addr string) error { // Apply middlewares finalHandler := http.Handler(r.Mux) @@ -111,11 +180,16 @@ type HTTPError struct { Message string } +// Error returns a string representation of the HTTPError. func (e *HTTPError) Error() string { return fmt.Sprintf("%d - %s", e.Code, e.Message) } -// NewHTTPError creates a new HTTPError instance. +// NewHTTPError creates a new HTTPError instance with the given status code and message. +// +// Example usage: +// +// err := NewHTTPError(http.StatusBadRequest, "Bad Request: missing parameters") func NewHTTPError(code int, message string) *HTTPError { return &HTTPError{ Code: code, @@ -160,7 +234,7 @@ func (r *Router) withErrorHandling(next http.HandlerFunc) http.HandlerFunc { } } -// withLogging logs the request method and path +// withLogging logs the request method and path. func (r *Router) withLogging(next http.HandlerFunc) http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { log.Printf("Received %s %s", r.Method, r.URL.Path) @@ -168,7 +242,75 @@ func (r *Router) withLogging(next http.HandlerFunc) http.HandlerFunc { } } -// ServeStatic creates a file server handler to serve static files +// CustomHandler is a function type that takes a custom Context. +type CustomHandler func(ctx *context.Context) + +// Get registers a handler for GET requests. +func (r *Router) Get(path string, handler CustomHandler) { + r.AddRoute(path, UnWrapCustomHandler(r.withContext(handler, "GET"))) +} + +// Post registers a handler for POST requests. +func (r *Router) Post(path string, handler CustomHandler) { + r.AddRoute(path, UnWrapCustomHandler(r.withContext(handler, "POST"))) +} + +// Put registers a handler for PUT requests. +func (r *Router) Put(path string, handler CustomHandler) { + r.AddRoute(path, UnWrapCustomHandler(r.withContext(handler, "PUT"))) +} + +// Delete registers a handler for DELETE requests. +func (r *Router) Delete(path string, handler CustomHandler) { + r.AddRoute(path, UnWrapCustomHandler(r.withContext(handler, "DELETE"))) +} + +// Patch registers a handler for PATCH requests. +func (r *Router) Patch(path string, handler CustomHandler) { + r.AddRoute(path, UnWrapCustomHandler(r.withContext(handler, "PATCH"))) +} + +// WrapCustomHandler converts a CustomHandler to http.HandlerFunc. +func WrapCustomHandler(handler CustomHandler) http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + ctx := context.NewContext(r, w) + handler(ctx) + } +} + +// UnWrapCustomHandler converts a http.HandlerFunc to CustomHandler. +func UnWrapCustomHandler(handler http.HandlerFunc) CustomHandler { + return func(ctx *context.Context) { + handler.ServeHTTP(ctx.Res, ctx.Req) + } +} + +// withContext wraps the given handler with a custom context. +// This provides utility methods for handling requests and responses. +// It transforms the original handler to use the custom Context. +// +// Example usage: +// +// r.AddRoute("/example", func(ctx *context.Context) { +// ctx.JSON(http.StatusOK, map[string]string{"message": "Hello, world!"}) +// }) +func (r *Router) withContext(next CustomHandler, method string) http.HandlerFunc { + return func(w http.ResponseWriter, req *http.Request) { + if req.Method != method { + http.Error(w, "Method Not Allowed", http.StatusMethodNotAllowed) + return + } + ctx := context.NewContext(req, w) + next(ctx) + } +} + +// ServeStatic creates a file server handler to serve static files from the given directory. +// The pathPrefix is stripped from the request URL before serving the file. +// +// Example usage: +// +// http.Handle("/static/", ServeStatic("/static/", "/path/to/static/files")) func ServeStatic(pathPrefix, dir string) http.Handler { // Resolve the absolute path for debugging absPath, err := filepath.Abs(dir) diff --git a/pkg/lessgo/less.go b/pkg/lessgo/less.go index c2ff4e5..fa83a5a 100644 --- a/pkg/lessgo/less.go +++ b/pkg/lessgo/less.go @@ -5,6 +5,7 @@ import ( "time" "github.com/hokamsingh/lessgo/internal/core/config" + "github.com/hokamsingh/lessgo/internal/core/context" "github.com/hokamsingh/lessgo/internal/core/controller" "github.com/hokamsingh/lessgo/internal/core/di" "github.com/hokamsingh/lessgo/internal/core/middleware" @@ -25,6 +26,7 @@ type Router = router.Router type BaseService = service.BaseService type Service = service.Service type CORSOptions = middleware.CORSOptions +type Context = context.Context // Expose middleware types and functions type CORSMiddleware = middleware.CORSMiddleware