stackable

package module
v0.0.11 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Nov 19, 2025 License: GPL-2.0 Imports: 8 Imported by: 0

README

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 (
	"errors"
	"log/slog"
	"net/http"
	"os"
	"sync/atomic"

	"github.com/saryginrodion/stackable"
)

// 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()

	slog.Info(
		"Request processed",
		"tag", s.tag,
		"rid", context.Local.requestId,
		"ip", context.Request.RemoteAddr,
	)

	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() {
	// Creating new Stackable with Shared instance
	stack := stackable.NewStackable[Shared, Local](
		new(Shared),
	)

	logger := slog.New(
		slog.NewTextHandler(os.Stdout, &slog.HandlerOptions{
			Level: slog.LevelDebug,
		}),
	)

	stack.SetLogger(logger)

	// 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",
			}
		}),
	))

	slog.Info("Starting...")
	err := http.ListenAndServe(":8000", nil)
	if err != nil {
		slog.Error("Error on http.ListenAndServer", "err", err)
	}
}

Documentation

Index

Constants

This section is empty.

Variables

This section is empty.

Functions

func DefaultValue

func DefaultValue[T Default]() T

Types

type Context

type Context[S ISharedState, L ILocalState] struct {
	Shared   *S
	Local    *L
	Response Response
	Request  *http.Request
}

type Default

type Default interface {
	Default() any
}

type FuncHandlerWrapper

type FuncHandlerWrapper[S ISharedState, L ILocalState] struct {
	Handler func(
		context *Context[S, L],
		next func() error,
	) error
}

func WrapFunc

func WrapFunc[S ISharedState, L ILocalState](handler func(
	context *Context[S, L],
	next func() error,
) error,
) FuncHandlerWrapper[S, L]

func (FuncHandlerWrapper[S, L]) Run

func (h FuncHandlerWrapper[S, L]) Run(
	context *Context[S, L],
	next func() error,
) error

type Handler

type Handler[S ISharedState, L ILocalState] interface {
	Run(context *Context[S, L], next func() error) error
}

type HeadersContainer

type HeadersContainer struct {
	// contains filtered or unexported fields
}

func NewHeadersContainer

func NewHeadersContainer() HeadersContainer

func (*HeadersContainer) Add

func (s *HeadersContainer) Add(key string, value string)

func (*HeadersContainer) Contains

func (s *HeadersContainer) Contains(key string) bool

func (*HeadersContainer) Delete

func (s *HeadersContainer) Delete(key string) []string

func (*HeadersContainer) Entries

func (s *HeadersContainer) Entries() iter.Seq2[string, []string]

func (*HeadersContainer) Get

func (s *HeadersContainer) Get(key string) []string

func (*HeadersContainer) Set

func (s *HeadersContainer) Set(key string, value string)

type HttpError

type HttpError struct {
	Status  int    `json:"status"`
	Message string `json:"message"`
}

func (HttpError) Error

func (e HttpError) Error() string

type HttpResponse

type HttpResponse struct {
	// contains filtered or unexported fields
}

func JsonResponse

func JsonResponse(status int, data any) (*HttpResponse, error)

func NewHttpResponse

func NewHttpResponse(status int, contentType string, body string) *HttpResponse

func NewHttpResponseRaw

func NewHttpResponseRaw(headers HeadersContainer, status int, body io.Reader) *HttpResponse

func (*HttpResponse) Body

func (r *HttpResponse) Body() io.Reader

func (*HttpResponse) Headers

func (r *HttpResponse) Headers() HeadersContainer

func (*HttpResponse) SetHeaders

func (r *HttpResponse) SetHeaders(newHeaders HeadersContainer)

func (*HttpResponse) Status

func (r *HttpResponse) Status() int

type ILocalState

type ILocalState interface {
	Default
}

type ISharedState

type ISharedState any

type Response

type Response interface {
	Headers() HeadersContainer
	Body() io.Reader
	Status() int
}

type Stackable

type Stackable[S ISharedState, L ILocalState] struct {
	Handlers []Handler[S, L]
	Shared   *S
	// contains filtered or unexported fields
}

func NewStackable

func NewStackable[S ISharedState, L ILocalState](s *S) Stackable[S, L]

func (*Stackable[S, L]) AddHandler

func (s *Stackable[S, L]) AddHandler(handler Handler[S, L]) *Stackable[S, L]

func (Stackable[S, L]) AddUniqueHandler

func (s Stackable[S, L]) AddUniqueHandler(handler Handler[S, L]) Stackable[S, L]

func (*Stackable[S, L]) HttpHandler

func (s *Stackable[S, L]) HttpHandler() http.HandlerFunc

func (Stackable[S, L]) ServeHTTP

func (s Stackable[S, L]) ServeHTTP(response http.ResponseWriter, request *http.Request)

func (*Stackable[S, L]) SetLogger added in v0.0.11

func (s *Stackable[S, L]) SetLogger(logger *slog.Logger)

Directories

Path Synopsis

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL