From 67667dd2ce3a94c567fda7d78b351b07d9ddb72c Mon Sep 17 00:00:00 2001 From: Ankit Patial Date: Sat, 12 Oct 2024 19:34:17 +0530 Subject: [PATCH] router with helper methods --- README.md | 114 ++++++++++++++++++++++++++++++++++++- example/main.go | 109 ++++++++++++++++++++++++++++++++++++ go.mod | 3 + resource.go | 99 +++++++++++++++++++++++++++++++++ router.go | 145 ++++++++++++++++++++++++++++++++++++++++++++++++ router_serve.go | 41 ++++++++++++++ 6 files changed, 509 insertions(+), 2 deletions(-) create mode 100644 example/main.go create mode 100644 go.mod create mode 100644 resource.go create mode 100644 router.go create mode 100644 router_serve.go diff --git a/README.md b/README.md index c91404c..8f15265 100644 --- a/README.md +++ b/README.md @@ -1,3 +1,113 @@ -# HTTP Request Multiplexer +# Mux -HTTP request multiplexer on top of Go inbuild http.ServeMux but with eas of defining routes, with some usefull middlewares. \ No newline at end of file +Tiny wrapper around Go's builtin http.ServeMux with easy routing methods. + +## Example + +```go +package main + +import ( + "log/slog" + "net/http" + + "gitserver.in/patialtech/mux" +) + +func main() { + // init mux router + r := mux.NewRouter() + + // here is how can add middlewares, these will apply to all routes after it + r.Use(middleware1, middleware2) + + // let's add a route + r.Get("/hello", func(w http.ResponseWriter, r *http.Request) { + w.Write([]byte("hello1")) + }) + // r.Post(pattern string, h http.HandlerFunc) + // r.Put(pattern string, h http.HandlerFunc) + // ... + + // you can inline middleware(s) to a route + r. + With(mwInline). + Get("/test", func(w http.ResponseWriter, r *http.Request) { + w.Write([]byte("middle ware 3 test")) + }) + + // you define a resource + r.Resource("/photos", func(resource *mux.Resource) { + // rails style routes + // GET /photos + // GET /photos/new + // POST /photos + // GET /photos/:id + // GET /photos/:id/edit + // PUT /photos/:id + // PATCH /photos/:id + // DELETE /photos/:id + resource.Index(func(w http.ResponseWriter, r *http.Request) { + w.Write([]byte("user index")) + }) + + resource.New(func(w http.ResponseWriter, r *http.Request) { + w.Write([]byte("create new user")) + }) + }) + + // you can create group of few routes with their own middlewares + r.Group(func(grp *mux.Router) { + grp.Use(mwGroup) + grp.Get("/group", func(w http.ResponseWriter, r *http.Request) { + w.Write([]byte("i am group 1")) + }) + }) + + // catche all + r.Get("/", func(w http.ResponseWriter, r *http.Request) { + w.Write([]byte("hello there")) + }) + + // Serve allows graceful shutdown + r.Serve(func(srv *http.Server) error { + srv.Addr = ":3001" + // srv.ReadTimeout = time.Minute + // srv.WriteTimeout = time.Minute + + slog.Info("listening on http://localhost" + srv.Addr) + return srv.ListenAndServe() + }) +} + +// +// example middlewares +// +func middleware1(h http.Handler) http.Handler { + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + slog.Info("i am middleware 1") + h.ServeHTTP(w, r) + }) +} + +func middleware2(h http.Handler) http.Handler { + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + slog.Info("i am middleware 2") + h.ServeHTTP(w, r) + }) +} + +func mwInline(h http.Handler) http.Handler { + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + slog.Info("i am inline middleware") + h.ServeHTTP(w, r) + }) +} + +func mwGroup(h http.Handler) http.Handler { + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + slog.Info("i am group middleware") + h.ServeHTTP(w, r) + }) +} +``` diff --git a/example/main.go b/example/main.go new file mode 100644 index 0000000..3843a64 --- /dev/null +++ b/example/main.go @@ -0,0 +1,109 @@ +package main + +import ( + "log/slog" + "net/http" + + "gitserver.in/patialtech/mux" +) + +package main + +import ( + "log/slog" + "net/http" + + "gitserver.in/patialtech/mux" +) + +func main() { + // init mux router + r := mux.NewRouter() + + // here is how can add middlewares, these will apply to all routes after it + r.Use(middleware1, middleware2) + + // let's add a route + r.Get("/hello", func(w http.ResponseWriter, r *http.Request) { + w.Write([]byte("hello1")) + }) + // r.Post(pattern string, h http.HandlerFunc) + // r.Put(pattern string, h http.HandlerFunc) + // ... + + // you can inline middleware(s) to a route + r. + With(mwInline). + Get("/test", func(w http.ResponseWriter, r *http.Request) { + w.Write([]byte("middle ware 3 test")) + }) + + // you define a resource + r.Resource("/photos", func(resource *mux.Resource) { + // rails style routes + // GET /photos + // GET /photos/new + // POST /photos + // GET /photos/:id + // GET /photos/:id/edit + // PUT /photos/:id + // PATCH /photos/:id + // DELETE /photos/:id + resource.Index(func(w http.ResponseWriter, r *http.Request) { + w.Write([]byte("user index")) + }) + + resource.New(func(w http.ResponseWriter, r *http.Request) { + w.Write([]byte("create new user")) + }) + }) + + r.Group(func(grp *mux.Router) { + grp.Use(mwGroup) + grp.Get("/group", func(w http.ResponseWriter, r *http.Request) { + w.Write([]byte("i am group 1")) + }) + }) + + // catches all + r.Get("/", func(w http.ResponseWriter, r *http.Request) { + w.Write([]byte("hello there")) + }) + + r.Serve(func(srv *http.Server) error { + srv.Addr = ":3001" + // srv.ReadTimeout = time.Minute + // srv.WriteTimeout = time.Minute + + slog.Info("listening on http://localhost" + srv.Addr) + return srv.ListenAndServe() + }) +} + +func middleware1(h http.Handler) http.Handler { + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + slog.Info("i am middleware 1") + h.ServeHTTP(w, r) + }) +} + +func middleware2(h http.Handler) http.Handler { + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + slog.Info("i am middleware 2") + h.ServeHTTP(w, r) + }) +} + +func mwInline(h http.Handler) http.Handler { + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + slog.Info("i am inline middleware") + h.ServeHTTP(w, r) + }) +} + +func mwGroup(h http.Handler) http.Handler { + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + slog.Info("i am group middleware") + h.ServeHTTP(w, r) + }) +} diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..3bd7d28 --- /dev/null +++ b/go.mod @@ -0,0 +1,3 @@ +module gitserver.in/patialtech/mux + +go 1.23.2 diff --git a/resource.go b/resource.go new file mode 100644 index 0000000..9cb1265 --- /dev/null +++ b/resource.go @@ -0,0 +1,99 @@ +package mux + +import ( + "fmt" + "net/http" + "strings" +) + +// Resource is a resourceful route provides a mapping between HTTP verbs and URLs and controller actions. +// By convention, each action also maps to particular CRUD operations in a database. +// A single entry in the routing file, such as +// Index route +// +// GET /resource-name # index route +// +// GET /resource-name/new # create resource page +// +// POST /resource-name # create resource post +// +// GET /resource-name/:id # view resource +// +// GET /resource-name/:id/edit # edit resource +// +// PUT /resource-name/:id # update resource +// +// DELETE /resource-name/:id # delete resource +type Resource struct { + mux *http.ServeMux + pattern string + middlewares []func(http.Handler) http.Handler +} + +func sufixIt(str, sufix string) string { + var p strings.Builder + p.WriteString(str) + if !strings.HasSuffix(str, "/") { + p.WriteString("/") + } + p.WriteString(sufix) + return p.String() +} + +// Index is GET /resource-name +func (r *Resource) Index(h http.HandlerFunc) { + r.handlerFunc(http.MethodGet, r.pattern, h) +} + +// Create is GET /resource-name/new +func (r *Resource) New(h http.HandlerFunc) { + r.handlerFunc(http.MethodGet, sufixIt(r.pattern, "new"), h) +} + +// Create is POST /resource-name +func (r *Resource) Create(h http.HandlerFunc) { + r.handlerFunc(http.MethodPost, r.pattern, h) +} + +// Show is GET /resource-name/:id +func (r *Resource) Show(h http.HandlerFunc) { + r.handlerFunc(http.MethodGet, sufixIt(r.pattern, "{id}"), h) +} + +// Update is PUT /resource-name/:id +func (r *Resource) Update(h http.HandlerFunc) { + r.handlerFunc(http.MethodPut, sufixIt(r.pattern, "{id}"), h) +} + +// Update is PATCH /resource-name/:id +func (r *Resource) PartialUpdate(h http.HandlerFunc) { + r.handlerFunc(http.MethodPatch, sufixIt(r.pattern, "{id}"), h) +} + +func (r *Resource) Destroy(h http.HandlerFunc) { + r.handlerFunc(http.MethodDelete, sufixIt(r.pattern, "{id}"), h) +} + +// HandleFunc registers the handler function for the given pattern. +// If the given pattern conflicts, with one that is already registered, HandleFunc +// panics. +func (r *Resource) handlerFunc(method, pattern string, h http.HandlerFunc) { + if r == nil { + panic("serve: func handlerFunc() was called on nil") + } + + if r.mux == nil { + panic("serve: router mux is nil") + } + + path := fmt.Sprintf("%s %s", method, pattern) + r.mux.Handle(path, stack(r.middlewares, h)) +} + +// Use appends one or more middlewares onto the Router stack. +func (r *Resource) Use(middlewares ...func(http.Handler) http.Handler) { + if r == nil { + panic("serve: func Use was called on nil") + } + r.middlewares = append(r.middlewares, middlewares...) +} diff --git a/router.go b/router.go new file mode 100644 index 0000000..3b79448 --- /dev/null +++ b/router.go @@ -0,0 +1,145 @@ +package mux + +import ( + "fmt" + "net/http" +) + +const RouteCtxKey = "ServeCTX" + +type ( + // Router is a wrapper arround the go's standard http.ServeMux. + // Its a lean wrapper with feature that are there to make easy use of it. + Router struct { + mux *http.ServeMux + middlewares []func(http.Handler) http.Handler + } +) + +// New instance of Mux +func NewRouter() *Router { + return &Router{ + mux: http.NewServeMux(), + } +} + +// Use appends one or more middlewares onto the Router stack. +func (r *Router) Use(h ...func(http.Handler) http.Handler) { + if r == nil { + panic("serve: func Use was called on nil") + } + r.middlewares = append(r.middlewares, h...) +} + +func (r *Router) Get(pattern string, h http.HandlerFunc) { + r.handlerFunc(http.MethodGet, pattern, h) +} + +func (r *Router) Head(pattern string, h http.HandlerFunc) { + r.handlerFunc(http.MethodHead, pattern, h) +} + +func (r *Router) Post(pattern string, h http.HandlerFunc) { + r.handlerFunc(http.MethodPost, pattern, h) +} + +func (r *Router) Put(pattern string, h http.HandlerFunc) { + r.handlerFunc(http.MethodPut, pattern, h) +} + +func (r *Router) Patch(pattern string, h http.HandlerFunc) { + r.handlerFunc(http.MethodPatch, pattern, h) +} + +func (r *Router) Delete(pattern string, h http.HandlerFunc) { + r.handlerFunc(http.MethodDelete, pattern, h) +} + +func (r *Router) Connect(pattern string, h http.HandlerFunc) { + r.handlerFunc(http.MethodConnect, pattern, h) +} + +func (r *Router) Options(pattern string, h http.HandlerFunc) { + r.handlerFunc(http.MethodOptions, pattern, h) +} + +func (r *Router) Trace(pattern string, h http.HandlerFunc) { + r.handlerFunc(http.MethodTrace, pattern, h) +} + +// HandleFunc registers the handler function for the given pattern. +// If the given pattern conflicts, with one that is already registered, HandleFunc +// panics. +func (r *Router) handlerFunc(method, pattern string, h http.HandlerFunc) { + if r == nil { + panic("serve: func Handle() was called on nil") + } + + path := fmt.Sprintf("%s %s", method, pattern) + r.mux.Handle(path, stack(r.middlewares, h)) +} + +// With adds inline middlewares for an endpoint handler. +func (r *Router) With(middleware ...func(http.Handler) http.Handler) *Router { + mws := make([]func(http.Handler) http.Handler, len(r.middlewares)) + copy(mws, r.middlewares) + mws = append(mws, middleware...) + + im := &Router{ + mux: r.mux, + middlewares: mws, + } + + return im +} + +// Group adds a new inline-Router along the current routing +// path, with a fresh middleware stack for the inline-Router. +func (r *Router) Group(fn func(grp *Router)) { + if fn == nil { + return + } + + im := r.With() + fn(im) +} + +// Route mounts a sub-Router along a `pattern“ string. +func (r *Router) Resource(pattern string, fn func(resource *Resource)) { + if r == nil { + panic(fmt.Sprintf("chi: attempting to Route() a nil subrouter on '%s'", pattern)) + } + + if fn == nil { + panic(fmt.Sprintf("chi: attempting to Resource() a nil subrouter on '%s'", pattern)) + } + + fn(&Resource{ + mux: r.mux, + pattern: pattern, + }) +} + +func (r *Router) ServeHTTP(w http.ResponseWriter, req *http.Request) { + if r == nil { + panic("method ServeHTTP called on nil") + } + + r.mux.ServeHTTP(w, req) +} + +// stack middlewares(http handler) in order they are passed (FIFO) +func stack(middlewares []func(http.Handler) http.Handler, endpoint http.Handler) http.Handler { + // Return ahead of time if there aren't any middlewares for the chain + if len(middlewares) == 0 { + return endpoint + } + + // wrap the end handler with the middleware chain + h := middlewares[len(middlewares)-1](endpoint) + for i := len(middlewares) - 2; i >= 0; i-- { + h = middlewares[i](h) + } + + return h +} diff --git a/router_serve.go b/router_serve.go new file mode 100644 index 0000000..42fabe3 --- /dev/null +++ b/router_serve.go @@ -0,0 +1,41 @@ +package mux + +import ( + "context" + "log/slog" + "net/http" + "os" + "os/signal" +) + +type ServeCB func(srv *http.Server) error + +// Serve with gracefull shutdown +func (r *Router) Serve(cb ServeCB) { + srv := &http.Server{ + Handler: r, + } + + idleConnsClosed := make(chan struct{}) + go func() { + sigint := make(chan os.Signal, 1) + signal.Notify(sigint, os.Interrupt) + <-sigint + + // We received an interrupt signal, shut down. + if err := srv.Shutdown(context.Background()); err != nil { + // Error from closing listeners, or context timeout: + slog.Error("server shutdown error", "error", err) + } else { + slog.Info("server shutdown") + } + close(idleConnsClosed) + }() + + if err := cb(srv); err != http.ErrServerClosed { + // Error starting or closing listener: + slog.Error("start server error", "error", err) + } + + <-idleConnsClosed +}