gorouter

package module
v1.2.2 Latest Latest
Warning

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

Go to latest
Published: Apr 22, 2024 License: MIT Imports: 17 Imported by: 1

README

goRouter

A simple, lightweight REST supporting Router implemented in Go.

Creating a new instance

In order to initiate a new router entity, call the New function with all desired functional parameters.

package main

import "github.com/balazskvancz/gorouter"

func main() {
  // A simple and functional router which will set all 
  // different features to default values. 
  // The port will be :8000, default 404 and HTTP OPTIONS method handler.
  // The by default response status will be 200,
  // and the maximum size of the a multipart/form-data will be 20Mb.
  r := gorouter.New()

  // Starts the listening. Be aware: this is a blocking method!
  r.Listen()
}

In case of using different settings, it is possible to decorate this factory method by passing various functions such as:

var customNotFoundHandler = func (ctx gorouter.Context) {
  type res struct {
    Msg string `json:"msg"`
  }

  data := res{
    Msg: "not found",
  }

  ctx.SendJson(&data)
}

r := gorouter.New(
  // Sets up the router's address to :3000
  gorouter.WithAddress(3000),
  // Maximum of 50Mb formData body.
  gorouter.WithMaxBodySize(50<<20),
  // Sets the method in case of not finding a matching route.
  gorouter.WithNotFoundHandler(customNotFoundHandler),
)
Listen

The basic mode to make the router start listening on the given port is by calling Listen(). It will be up and running until the context receives termination signal.

ListenWithContext

The other way of starting the listening is by calling ListenWithContext, where you can pass a context as a parameter, which could have a cancellation or a timeout. Remember, is also ends the listening in case of an interrupt or a sigterm signal.

r := gorouter.New()

ctx, cancel = context.WithCancel(context.Background())

go r.ListenWithContext(ctx)

// some logic...
cancel() // It stops the running of the router.

Registering endpoints

Currently only five method types are implemented – there is no technical limit, I just dont use the others :) – and these are: GET, POST, PUT, DELETE and HEAD. Each of these methods have a correspondig method attached to the router instance, where one can register an endpoint by the given URL and the handler function itself.

Of course, there is possibility to register routes with wildcard path parameters, which are essential in REST.

r := gorouter.New()

r.Get("/api/products/{id}", func (ctx Context) {
  id := ctx.GetParam("id")
  ctx.WriteResponse([]byte(id))
})

r.Post("/api/products/{id}", func (ctx Context) {
  // doing some logic...
  ctx.SendOk() // Normal HTTP 200 response.
})

Every endpoint can have multiple middlewares that are executed before the registered handler. Keep in mind, if at least on middleware would not call the next function, then the handler – and also the remaining middlewares – wont be executed.

r := gorouter.New()

r.Get("/api/products/{id}", func (ctx Context) {
  id := ctx.GetParam("id")
	ctx.WriteResponse([]byte(id))
}).RegisterMiddlewares(func(ctx gorouter.Context, next gorouter.HandlerFunc) {
  fmt.Println(ctx.GetUrl())
  next(ctx)
})

// The `RegisterMiddlewares` method takes a slice of MiddlewareFunc as parameters, 
// so you can register multiple middlewares at the same time.
// However, the execution order will be the same as the order of the registration.

var (
  // Example of a MiddlewareFunc.
  mw1 = func (ctx gorouter.Context, next gorouter.HandlerFunc) {
    fmt.Println(ctx.GetUrl())
    next(ctx)
  }

  mw2 = func (ctx gorouter.Context, next gorouter.HandlerFunc) {
    fmt.Println("Another one")
    next(ctx)
  }

  r.Get("/api/welcome", func (ctx Context) {
	  ctx.WriteResponse([]byte("Welcome to this API!"))
  }).RegisterMiddlewares(mw2, mw1) // So the executes order will be the mw2, mw1 and the handler.
)

Global middlewares

Beside the middleware functions that are attached to certain endpoints by registering it explicitly, there is a way to register middlewares on a global level. These middlewares are consists of two main parts: the first one is the prementioned MiddlewareFunc, and the second is the matcher – or matchers.

The MiddlewareFunc is responsible for doing the actual middleware logic and matcher is for determining if a Middleware is opt for that certain Context.

Lets take an example here. You want a globally registered middleware which authorizes the request. You dont want every request to be examined, only if the route contains a the phrase admin in it.

You could achive this by creating your middleware like this:

adminMWFn := func(ctx gorouter.Context, next gorouter.HandlerFunc) {
  if !strings.Contains(ctx.GetUrl(), "admin") {
    next(ctx)
    return
  }
  if isAdmin := isAdmin(); isAdmin {
    next(ctx)
    return
  }
  // Logging or any other activities in case of unsuccessful authorization.
}

adminMw := gorouter.NewMiddleware(adminMwFn)
// ...

However, this router takes a different approach. I believe, a MiddlewareFunc should be responsible only for the main logic and not for determining if the execution should take place or not.

Example:

adminMWFn := func(ctx gorouter.Context, next gorouter.HandlerFunc) {
  if isAdmin := isAdmin(); isAdmin {
    next(ctx)
    return
  }
  // Logging or any other activities in case of unsuccessful authorization.
}

adminMwMatcher := func(ctx gorouter.Context) bool {
  return strings.Contains(ctx.GetUrl(), "admin") 
}

adminMw := gorouter.NewMiddleware(adminMwFn, adminMwMatcher)
// ...

This way you can separately test your MiddlewareFunc and also the matcherFunc if it is complex.

The NewMiddleware creates a new Middleware for global registration, and if you dont provide any matcher(s) then, the default one would be used, which means, it would match for each and every context.

Also, it is possible to register more than one matchers to a global Middleware. In this case, every matcher should be matching. This one is for code reusability.

The execution order follows this pattern:

  • execute all global – preRunner – middlewares that are matching for the Context,
  • looking up the registered handler from the tree based upon the method and the URL,
  • executing the attached middlewares to the endpoint – if there is any,
  • executing the handler,
  • executing all global – postRunner – middlewares that are matching for the Context.
Pre and PostRunner global middlewares

The only difference between the these middlewares are the registration function.

r := gorouter.New()

preMw := gorouter.NewMiddleware(func(ctx gorouter.Context, next gorouter.HandlerFunc) {
  // pre logic.
  next(ctx)
})

postMw := gorouter.NewMiddleware(func(ctx gorouter.Context, next gorouter.HandlerFunc) {
  // post logic.
  next(ctx)
})

r.RegisterMiddlewares(preMw)
r.RegisterPostMiddlewares(postMw)

Context

The main way to interacting with the incoming request and the response is done by the abstraction that the Context implements. From reading the incoming postData to writing the response, everything this done by this interface.

Every HandlerFunc we want to register as a handler endpoint, should have a function signature like this:

func doSomeThingHandler(ctx gorouter.Context) {
  // 
}

Documentation

Index

Constants

View Source
const (
	JsonContentType          string = "application/json"
	JsonContentTypeUTF8      string = JsonContentType + "; charset=UTF-8"
	TextHtmlContentType      string = "text/html"
	MultiPartFormContentType string = "multipart/form-data"
)
View Source
const (
	InfoLogLevel logTypeValue = 1 << iota
	WarningLogLevel
	ErrorLogLevel
)
View Source
const (
	MiddlewarePreRunner  middlewareType = "preRunner"
	MiddlewarePostRunner middlewareType = "postRunner"
)

Variables

View Source
var (
	ErrCtNotMultipart = errors.New("the content-type is not multipart/form-data")
)

Functions

func NewContext added in v1.1.0

func NewContext(conf ContextConfig) *context

NewContext creates and returns a new context.

func WithAddress

func WithAddress(address int) routerOptionFunc

WithAddress allows to configure address of the router where it will be listening.

func WithBodyReader

func WithBodyReader(reader bodyReaderFn) routerOptionFunc

WithBodyReader allows to configure a default body reader function or disable it (by passing in <nil>).

func WithContext

func WithContext(ctx ctxpkg.Context) routerOptionFunc

WithContext allows to configure basecontext of the router which will be passed to each and every handler.

func WithDefaultStatusCode

func WithDefaultStatusCode(statusCode int) routerOptionFunc

WithDefaultStatusCode allows to configure the default statusCode of the response without specifying it explicitly.

func WithEmptyTreeHandler added in v1.2.0

func WithEmptyTreeHandler(handler HandlerFunc) routerOptionFunc

WithEmptyTreeHandler allows to configure the handler in case of an empty method tree event.

func WithMaxBodySize

func WithMaxBodySize(size int64) routerOptionFunc

WithMaxBodySize allows to configure maximum size incoming, decodable formdata.

func WithMiddlewaresEnabled added in v1.2.0

func WithMiddlewaresEnabled(areEnabled bool) routerOptionFunc

WithMiddlewaresEnabled allows to configure the state of the middlewares.

func WithNotFoundHandler

func WithNotFoundHandler(h HandlerFunc) routerOptionFunc

WithNotFoundHandler allows to configure 404 handler of the router.

func WithOptionsHandler

func WithOptionsHandler(h HandlerFunc) routerOptionFunc

WithOptionsHandler allows to configure OPTIONS method handler of the router.

func WithPanicHandler

func WithPanicHandler(h PanicHandlerFunc) routerOptionFunc

WithPanicHandler allows to configure a recover function which is called if a panic happens somewhere.

func WithServerName

func WithServerName(name string) routerOptionFunc

WithServerName allows to configure the server name of the instance.

Types

type Context

type Context interface {
	Logger

	// ---- Methods about the Context itself.
	Reset(http.ResponseWriter, *http.Request)
	Empty()
	GetContextId() uint64

	// ---- Request
	GetRequest() *http.Request
	GetRequestMethod() string
	GetUrl() string
	GetCleanedUrl() string
	GetQueryParams() url.Values
	GetQueryParam(string) string
	BindValue(ContextKey, any)
	GetBindedValue(ContextKey) any
	GetRequestHeader(string) string
	GetContentType() string
	GetParam(string) string
	GetParams() pathParams
	GetRequestHeaders() http.Header
	GetBody() []byte
	ParseForm() error
	GetFormFile(string) (File, error)
	GetFormValue(string) (string, error)

	// ---- Response
	SendJson(anyValue, ...int)
	SendNotFound()
	SendInternalServerError()
	SendMethodNotAllowed()
	SendOk()
	SendUnauthorized()
	SendRaw([]byte, int, http.Header)
	Pipe(*http.Response)
	SetStatusCode(int)
	WriteResponse(b []byte)
	AppendHttpHeader(header http.Header)
	WriteToResponseNow()
	Copy(io.Reader)

	GetLog() *contextLog
}

type ContextConfig added in v1.1.0

type ContextConfig struct {
	ContextIdChannel          contextIdChan
	DefaultResponseStatusCode int
	MaxIncomingBodySize       int64
	Logger                    Logger
}

type ContextKey added in v1.0.3

type ContextKey string

type File

type File interface {
	GetName() string
	GetSize() int64
	Close() error
	WriteTo(io.Writer) (int64, error)
	SaveTo(string) error
}

type HandlerFunc

type HandlerFunc func(Context)

type Logger

type Logger interface {
	Info(string, ...any)
	Error(string, ...any)
	Warning(string, ...any)
}

type Middleware

type Middleware interface {
	DoesMatch(Context) bool
	Execute(Context, HandlerFunc)
	IsAlwaysAllowed() bool
}

func NewMiddleware

func NewMiddleware(handler MiddlewareFunc, opts ...MiddlewareOptionFunc) Middleware

NewMiddleware creates and returns a new middleware based upon the given MiddlewareFunc and matchers.

type MiddlewareFunc

type MiddlewareFunc func(Context, HandlerFunc)

type MiddlewareMatcherFunc

type MiddlewareMatcherFunc func(Context) bool

type MiddlewareOptionFunc added in v1.2.0

type MiddlewareOptionFunc func(*middleware)

func MiddlewareWithAlwaysAllowed added in v1.2.0

func MiddlewareWithAlwaysAllowed(isAlwaysAllowed bool) MiddlewareOptionFunc

MiddlewareWithAlwaysAllowed configures, whether a middleware should run even if the middlewares are globally disallowed.

func MiddlewareWithMatchers added in v1.2.0

func MiddlewareWithMatchers(matchers ...MiddlewareMatcherFunc) MiddlewareOptionFunc

MiddlewareWithMatchers allows to configure the matchers for a given middleware.

type PanicHandlerFunc

type PanicHandlerFunc func(Context, interface{})

type Route

type Route interface {
	RegisterMiddlewares(...MiddlewareFunc) Route
	GetUrl() string
	// contains filtered or unexported methods
}

type Router

type Router interface {
	ServeHTTP(http.ResponseWriter, *http.Request)

	Serve(Context)
	ListenWithContext(ctxpkg.Context)
	Listen()
	RegisterMiddlewares(middlewares ...Middleware)
	RegisterPostMiddlewares(middlewares ...Middleware)

	// All the available methods to register:
	Get(string, HandlerFunc) Route
	Post(string, HandlerFunc) Route
	Put(string, HandlerFunc) Route
	Delete(string, HandlerFunc) Route
	Head(string, HandlerFunc) Route
}

func New

func New(opts ...routerOptionFunc) Router

New returns a new Router instance decorated by the given optionFuncs.

Jump to

Keyboard shortcuts

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