Skip to content

saryginrodion/stackable

Repository files navigation

Stackable

Stackable provides a way to stack multiple handlers together and use this stack as http.Handler.

Example with different middlewares and handlers:

package main
 
 import (
	"github.com/saryginrodion/stackable"
	"errors"
	"net/http"
	"sync/atomic"

	"github.com/sirupsen/logrus"
)


// Every handler in handlers stack has access to shared state - there you can store your DB connections and stuff like this.
type Shared struct {
	requestCounter atomic.Int32
}

// LocalState struct instantiated for every request - you can store unique to request data (e. g. user struct got from auth layer)
type Local struct {
	requestId int32
}

// LocalState needs to implement Default interface - with this values it will be instantiated for every request.
func (s Local) Default() any {
	return Local{
		// This field will be changed in requestId handler
		requestId: 0,
	}
}

// For readability, you can declare Context type.
type Context = stackable.Context[Shared, Local]

// Handlers (or layers) in stack is values implementing stackable.Handler[S, L] interface
// SetRequestIdMiddleware will be function Handler - it does not need to store anything, everything for it already saved in Shared.
var SetRequestIdMiddleware = stackable.WrapFunc(
	// context - holds Requst, Response, Shared and Local
	// next - function to call next handler in stack
	func(context *Context, next func() error) error {
		context.Local.requestId = context.Shared.requestCounter.Load()
		context.Shared.requestCounter.Add(1)

		// If you want to call next handler - use return next().
		// If no, you can return nil (handler succeed) or error.
		// When last handler in stack calls next it will return nil.
		return next()
	},
)

// Example of struct middleware
type LoggingMiddleware struct {
	tag string
}

func (s *LoggingMiddleware) Run(context *Context, next func() error) error {
	// Calling next() first to apply every layer below
	err := next()

	logrus.WithFields(
		logrus.Fields{
			"tag": s.tag, // Layers also can store some values inside them, like Shared, but only for the Handler instance
			"rid": context.Local.requestId,
			"ip":  context.Request.RemoteAddr,
		},
	).Infof("%d - %s %s", context.Response.Status(), context.Request.Method, context.Request.URL.Path)

	return err
}

// Layer for mapping errors to Json objects.
var ErrorMapperMiddleware = stackable.WrapFunc(
	func(context *Context, next func() error) error {
		err := next()
		if err != nil {
			var httpErr stackable.HttpError
			if errors.As(err, &httpErr) {
			} else {
				httpErr = stackable.HttpError{
					Status:  http.StatusInternalServerError,
					Message: err.Error(),
				}
			}
			context.Response, _ = stackable.JsonResponse(
				httpErr.Status,
				httpErr,
			)
			return nil
		}
		return err
	},
)

func main() {
	logrus.SetLevel(logrus.InfoLevel)

	// Creating new Stackable with Shared instance
	stack := stackable.Stackable[Shared, Local]{
		Shared: new(Shared),
	}

	// Handlers run from the first added to last
	// AddHandler adds handler to existing Stackable
	// With AddHandler you can add some layers, that will be applied for every request (if you are using this stack)
	stack.AddHandler(SetRequestIdMiddleware)
	stack.AddHandler(ErrorMapperMiddleware)

	// AddUniqueHandler is copying Stackable instance and adds new Handler to this. Stackable in stack will not be touched
	http.Handle("GET /", stack.AddUniqueHandler(&LoggingMiddleware{tag: "index route"}).AddUniqueHandler(
		stackable.WrapFunc(func(context *Context, next func() error) error {
			// Writing response
			context.Response = stackable.NewHttpResponse(
				http.StatusOK,
				"text/html",
				"<h1>Index route!</h1>",
			)
			return next()
		}),
	))

	// This handler will not use LoggingMiddleware.
	http.Handle("GET /json", stack.AddUniqueHandler(
		stackable.WrapFunc(func(context *Context, next func() error) error {
			context.Response, _ = stackable.JsonResponse(
				http.StatusOK,
				struct {
					Message string `json:"msg"`
				}{
					Message: "Hello World!",
				},
			)
			return next()
		}),
	))

	// We can throw errors!
	http.Handle("GET /error", stack.AddUniqueHandler(&LoggingMiddleware{tag: "error route"}).AddUniqueHandler(
		stackable.WrapFunc(func(context *Context, next func() error) error {
			return stackable.HttpError{
                Status: http.StatusTeapot,
                Message: "I AM A TEAPOT",
            }
		}),
	))

	logrus.Info("Starting...")
	err := http.ListenAndServe(":8000", nil)
	if err != nil {
		logrus.Fatal(err)
	}
}

About

Go lib for stackable http handlers (like expressjs middlewares)

Resources

License

Stars

Watchers

Forks

Packages

No packages published

Languages