simba

package module
v0.2.0 Latest Latest
Warning

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

Go to latest
Published: Dec 5, 2024 License: MIT Imports: 18 Imported by: 0

README

Simba

Simba is a lightweight, type-safe HTTP router framework for Go that makes building REST APIs simple and enjoyable. It provides strong type safety through generics and a clean, intuitive API for handling HTTP requests.

Features

  • Type-safe routing with Go generics
  • Built-in authentication support
  • Middleware support
  • Strong request/response typing
  • High performance through httprouter

Installation

go get github.com/sillen102/simba

Quick Start

Here's a simple example showing how to create a basic HTTP server with Simba:

package main

import (
    "context"
    "fmt"
    "net/http"
    "github.com/sillen102/simba"
)

type RequestBody struct {
    Name string `json:"name"`
    Age  int    `json:"age"`
}

type ResponseBody struct {
    Message string `json:"message"`
}

func handler(ctx context.Context, req *simba.Request[RequestBody, simba.NoParams]) (*simba.Response, error) {

    // Access the request body fields
    // req.Body.Age
    // req.Body.Name

    // Access the request cookies
    // req.Cookies

    // Access the request headers
    // req.Headers

    return &simba.Response{
        Headers: map[string][]string{"My-Header": {"header-value"}},
        Cookies: []*http.Cookie{{Name: "My-Cookie", Value: "cookie-value"}},
        Body: ResponseBody{
            Message: fmt.Sprintf("Hello %s, you are %d years old", req.Body.Name, req.Body.Age),
        },
        Status: http.StatusOK, // Can be omitted, defaults to 200 if there's a body, 204 if there's no body
    }, nil
}

func main() {
    // Using simba.Default() will use the default options for logging and request validation,
    // add default middleware like panic recovery and request id and add some endpoints like /health
    //
    // If you wish to build up your own router without any default middleware etc., use simba.New()
    app := simba.Default()
	app.Router.POST("/users", simba.HandlerFunc(handler))
    http.ListenAndServe(":9999", app)
}

Parameters

Handle parameters with type safety and validation support using go-playground validator:

type Params struct {
    UserID string `path:"userId"`
    Name   string `query:"name" validate:"required"`
    Age    int    `header:"age" validate:"required"`
    Page   int64  `query:"page" validate:"omitempty,min=0" default:"0"`
    Size   int64  `query:"size" validate:"omitempty,min=0" default:"10"`
}

func getUser(ctx context.Context, req *simba.Request[simba.NoBody, Params]) (*simba.Response, error) {
    userID := req.Params.UserID
    name := req.Params.Name
    age := req.Params.Age

    // ... handle the request
}

app.GET("/users/:userId", simba.HandlerFunc(getUser))

Logging

Simba automatically injects a zerolog logger into every request's context. You can access this logger from any handler or middleware using the logging package:

func handler(ctx context.Context, req *simba.Request[simba.NoBody, simba.NoParams]) (*simba.Response, error) {
    logger := logging.FromCtx(ctx)
    logger.Info().Msg("handling request")
    // ... handle the request
}

Configuration

Customize behavior with options:

app := simba.New(simba.Options{
    RequestDisallowUnknownFields: true,
    RequestIdAcceptHeader:        true,
    LogRequestBody:               true,
    LogLevel:                     zerolog.DebugLevel,
    LogFormat:                    logging.JsonFormat,
    LogOutput:                    os.Stdout,
})

Error Handling

Simba provides automatic error handling with standardized JSON responses. All errors are automatically wrapped and returned in a consistent format:

{
  "timestamp": "2024-12-04T20:28:33.852965Z",
  "status": 400,
  "error": "Bad Request",
  "path": "/api/resource",
  "method": "POST",
  "requestId": "01JE61MX24YEGF08E8G0RA0Y14",
  "message": "invalid request body",
  "validationErrors": [
    {
      "parameter": "email",
      "type": "body",
      "message": "'email' is not a valid email address"
    }
  ]
}

Middleware

Simba supports middleware. Simply create a function that takes a handler and returns a handler and register it with the Use method on the router:

func myMiddleware(next http.Handler) http.Handler {
	return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		r.Header.Set("X-Middleware", "123") // Here we simply add a header to every request
		next.ServeHTTP(w, r) // And the proceed to the next handler
	})
}

app.Router.Use(myMiddleware)

Authentication

Simba provides built-in support for authentication:

type User struct {
    ID   string
    Name string
}

func authFunc(r *http.Request) (*User, error) {
    // Your authentication logic here, either be it a database lookup or any other authentication method
    return &User{ID: "123", Name: "John"}, nil
}

// This handler will only be called if the user is authenticated and the user is available as one of the function parameters
func getUser(ctx context.Context, req *simba.Request[simba.NoBody, simba.NoParams], user *User) (*simba.Response, error) {
    // ... handle the request
}

app := simba.DefaultWithAuth[User](authFunc)
app.GET("/users/:userId", simba.AuthenticatedHandlerFunc(getUser))

Contributing

Contributions are welcome! Please feel free to submit a Pull Request.

License

This project is licensed under the MIT License - see the LICENSE file for details.

All dependencies are under their respective licenses, which can be found in their repositories via the go.mod file.

Documentation

Index

Constants

This section is empty.

Variables

This section is empty.

Functions

func AuthenticatedHandlerFunc

func AuthenticatedHandlerFunc[RequestBody any, Params any, AuthModel any](h AuthenticatedHandler[RequestBody, Params, AuthModel]) http.Handler

AuthenticatedHandlerFunc returns an http.Handler that can be used for authenticated routes

func HandleError added in v0.1.1

func HandleError(w http.ResponseWriter, r *http.Request, err error)

HandleError is a helper function for handling errors in HTTP handlers

func HandlerFunc

func HandlerFunc[RequestBody any, Params any](h Handler[RequestBody, Params]) http.Handler

HandlerFunc returns an http.Handler that can be used for non-authenticated routes

Types

type Application

type Application[AuthModel any] struct {
	// Router is the main router for the application
	Router *Router
	// contains filtered or unexported fields
}

Application is the main application struct that holds the router and other application settings

func Default

func Default() *Application[struct{}]

Default returns a new Application application with default settings

func DefaultWithAuth

func DefaultWithAuth[AuthModel any](authFunc AuthFunc[AuthModel]) *Application[AuthModel]

DefaultWithAuth returns a new Application application with default settings and ability to have authenticated routes using the provided authFunc to authenticate and retrieve the user

func New

func New(settings ...Settings) *Application[struct{}]

New returns a new Application application

func NewWithAuth

func NewWithAuth[User any](authFunc AuthFunc[User], st ...Settings) *Application[User]

NewWithAuth returns a new Application application with ability to have authenticated routes using the provided AuthFunc to authenticate and retrieve the authenticated model

func (*Application[AuthModel]) ServeHTTP added in v0.2.0

func (s *Application[AuthModel]) ServeHTTP(w http.ResponseWriter, req *http.Request)

ServeHTTP implements the http.Handler interface for the Simba Application

type AuthFunc

type AuthFunc[AuthModel any] func(r *http.Request) (*AuthModel, error)

AuthFunc is a function type for authenticating and retrieving an authenticated model struct from a request

type AuthenticatedHandler

type AuthenticatedHandler[RequestBody any, Params any, AuthModel any] func(ctx context.Context, req *Request[RequestBody, Params], user *AuthModel) (*Response, error)

AuthenticatedHandler handles a request with the request body and params.

Example usage:

Define a request body struct:

type RequestBody struct {
	Test string `json:"test" validate:"required"`
}

Define a request params struct:

type Params struct {
	Name   string `header:"name" validate:"required"`
	ID     int    `path:"id" validate:"required"`
	Active bool   `query:"active" validate:"required"`
	Page   int64  `query:"page" validate:"min=0"`
	Size   int64  `query:"size" validate:"min=0"`
}

Define a user struct:

type AuthModel struct {
	ID   int
	Name string
	Role string
}

Define a handler function:

func(ctx context.Context, req *simba.Request[RequestBody, Params], user *AuthModel) (*simba.Response, error) {
	// Access the request body and params fields
	req.Body.Test
	req.Params.Name
	req.Params.ID
	req.Params.Page
	req.Params.Size

	// Access the user fields
	user.ID
	user.Name
	user.Role

	// Return a response
	return &simba.Response{
		Headers: map[string][]string{"My-Header": {"header-value"}},
		Cookies: []*http.Cookie{{Name: "My-Cookie", Value: "cookie-value"}},
		Body:    map[string]string{"message": "success"},
		Status:  http.StatusOK,
	}, nil
}

Register the handler:

router.POST("/test/:id", simba.AuthenticatedHandlerFunc(handler))

func (AuthenticatedHandler[RequestBody, Params, AuthModel]) ServeHTTP

func (h AuthenticatedHandler[RequestBody, Params, AuthModel]) ServeHTTP(w http.ResponseWriter, r *http.Request)

ServeHTTP implements the http.Handler interface for AuthenticatedHandler

type ErrorResponse

type ErrorResponse struct {
	// Timestamp of the error
	Timestamp time.Time `json:"timestamp"`
	// HTTP status code
	Status int `json:"status"`
	// HTTP error type
	Error string `json:"error"`
	// Path of the request
	Path string `json:"path"`
	// Method of the request
	Method string `json:"method"`
	// Request ID
	RequestID string `json:"requestId,omitempty"`
	// Error message
	Message string `json:"message,omitempty"`
	// Validation errors
	ValidationErrors []ValidationError `json:"validationErrors,omitempty"`

} // @Name ErrorResponse

ErrorResponse defines the structure of an error message @Description Represents the structure of an error message returned by the API

func NewErrorResponse

func NewErrorResponse(r *http.Request, status int, message string, validationErrors ...ValidationError) *ErrorResponse

NewErrorResponse creates a new ErrorResponse instance with the given status and message

type HTTPError

type HTTPError struct {
	HttpStatusCode int
	Message        string

	ValidationErrors ValidationErrors
	// contains filtered or unexported fields
}

func NewHttpError

func NewHttpError(httpStatusCode int, publicMessage string, err error, validationErrors ...ValidationError) *HTTPError

NewHttpError creates a new ApiError

func WrapErrorHTTP

func WrapErrorHTTP(httpStatusCode int, err error, message string) *HTTPError

WrapErrorHTTP wraps an error with an HTTP status code

func (*HTTPError) Error

func (e *HTTPError) Error() string

Error implements the error interface and returns the full error details

func (*HTTPError) HasValidationErrors

func (e *HTTPError) HasValidationErrors() bool

HasValidationErrors checks if there are validation errors

func (*HTTPError) Unwrap

func (e *HTTPError) Unwrap() error

Unwrap returns the underlying error

type Handler

type Handler[RequestBody any, Params any] func(ctx context.Context, req *Request[RequestBody, Params]) (*Response, error)

Handler handles a request with the request body and params.

Example usage:

Define a request body struct:

type RequestBody struct {
	Test string `json:"test" validate:"required"`
}

Define a request params struct:

type Params struct {
	Name   string `header:"name" validate:"required"`
	ID     int    `path:"id" validate:"required"`
	Active bool   `query:"active" validate:"required"`
	Page   int64  `query:"page" validate:"min=0"`
	Size   int64  `query:"size" validate:"min=0"`
}

Define a handler function:

func(ctx context.Context, req *simba.Request[RequestBody, Params]) (*simba.Response, error) {
	// Access the request body and params fields
	req.Body.Test
	req.Params.Name
	req.Params.ID
	req.Params.Page
	req.Params.Size

	// Return a response
	return &simba.Response{
		Headers: map[string][]string{"My-Header": {"header-value"}},
		Cookies: []*http.Cookie{{Name: "My-Cookie", Value: "cookie-value"}},
		Body:    map[string]string{"message": "success"},
		Status:  http.StatusOK,
	}, nil
}

Register the handler:

router.POST("/test/:id", simba.HandlerFunc(handler))

func (Handler[RequestBody, Params]) ServeHTTP

func (h Handler[RequestBody, Params]) ServeHTTP(w http.ResponseWriter, r *http.Request)

ServeHTTP implements the http.Handler interface for Handler

type NoBody

type NoBody struct {
}

NoBody is an empty struct used to represent no body

type NoParams

type NoParams struct {
}

NoParams is an empty struct used to represent no params

type ParameterType

type ParameterType string
const (
	ParameterTypeHeader ParameterType = "header"
	ParameterTypePath   ParameterType = "path"
	ParameterTypeQuery  ParameterType = "query"
	ParameterTypeBody   ParameterType = "body"
)

type Request

type Request[RequestBody any, RequestParams any] struct {
	Cookies []*http.Cookie
	Body    RequestBody
	Params  RequestParams
}

Request represents a HTTP request

type Response

type Response struct {
	Headers http.Header
	Cookies []*http.Cookie
	Body    any
	Status  int
}

Response represents a HTTP response

type Router added in v0.2.0

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

Router is a simple router that wraps httprouter.Router and allows for middleware chaining

func (*Router) DELETE added in v0.2.0

func (r *Router) DELETE(path string, handler http.Handler)

DELETE registers a handler for DELETE requests to the given pattern

func (*Router) Extend added in v0.2.0

func (r *Router) Extend(middleware alice.Chain)

Extend extends the middleware chain with another chain

func (*Router) GET added in v0.2.0

func (r *Router) GET(path string, handler http.Handler)

GET registers a handler for GET requests to the given pattern

func (*Router) HEAD added in v0.2.0

func (r *Router) HEAD(path string, handler http.Handler)

HEAD registers a handler for HEAD requests to the given pattern

func (*Router) OPTIONS added in v0.2.0

func (r *Router) OPTIONS(path string, handler http.Handler)

OPTIONS registers a handler for OPTIONS requests to the given pattern

func (*Router) PATCH added in v0.2.0

func (r *Router) PATCH(path string, handler http.Handler)

PATCH registers a handler for PATCH requests to the given pattern

func (*Router) POST added in v0.2.0

func (r *Router) POST(path string, handler http.Handler)

POST registers a handler for POST requests to the given pattern

func (*Router) PUT added in v0.2.0

func (r *Router) PUT(path string, handler http.Handler)

PUT registers a handler for PUT requests to the given pattern

func (*Router) ServeHTTP added in v0.2.0

func (r *Router) ServeHTTP(w http.ResponseWriter, req *http.Request)

ServeHTTP implements the http.Handler interface for the Router type

func (*Router) Use added in v0.2.0

func (r *Router) Use(middleware func(next http.Handler) http.Handler)

Use registers a middleware handler

type Settings added in v0.2.0

type Settings struct {
	// RequestDisallowUnknownFields will disallow unknown fields in the request body,
	// resulting in a 400 Bad Request response if a field is present that cannot be
	// mapped to the model struct.
	RequestDisallowUnknownFields bool

	// RequestIdHeader will determine if the request ID should be read from the
	// request header. If not set, the request ID will be generated.
	RequestIdAcceptHeader bool

	// LogRequestBody will determine if the request body will be logged
	LogRequestBody bool

	// LogLevel is the log level for the logger that will be used
	LogLevel zerolog.Level

	// LogFormat is the log format for the logger that will be used
	LogFormat logging.LogFormat

	// LogOutput is the output for the logger that will be used
	// If not set, the output will be [os.Stdout]
	LogOutput io.Writer
}

Settings is a struct that holds the application settings

type ValidationError

type ValidationError struct {
	// Parameter that failed validation
	Parameter string `json:"parameter"`
	// Type indicates where the parameter was located (header, path, query, body)
	Type ParameterType `json:"type"`
	// Error message describing the validation error
	Message string `json:"message"`

} // @Name ValidationError

ValidationError defines the interface for a validation error @Description Detailed information about a validation error

type ValidationErrors

type ValidationErrors []ValidationError

ValidationErrors represents multiple validation errors

func (ValidationErrors) Error

func (ve ValidationErrors) Error() string

Error implements the error interface

Directories

Path Synopsis
websocket module

Jump to

Keyboard shortcuts

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