jug

package module
v0.4.1 Latest Latest
Warning

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

Go to latest
Published: Jan 4, 2025 License: MIT Imports: 10 Imported by: 2

README

Jug

A small shim for building opinionated REST services in go.

User Guide

TL;DR

This library puts a thin layer on top of the gin library. It's aimed at writing REST based web services that mainly use JSON representations.

Quick Start

Install dependencies.

go get github.com/cfichtmueller/jug

Write your server.

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

// Create a new router
router := jug.New()

// Set up routes

router.GET("/api/hello", func(c jug.Context) {
	c.RespondOk(Message{M: "Hello World"})
})

router.GET("/not-found", func(c jug.Context) {
	c.RespondNotFound(nil) // no response body
})

// Set up method not allowed handlers for all unset verbs on configured routes
router.ExpandMethods()

// Start the server
if err := router.Run("0.0.0.0:8000"); err != nil {
	log.Fatal(err)
}

User Guide

Setting up Routes
// set up a router
router := jug.New()

// handle GET /api/users
router.GET("/api/users", func(c jug.Context) {
	c.RespondOk(nil)
})

// create a sub group
projects := router.Group("/api/projects")

// handle GET /api/projects
projects.GET("", handler)
// handle POST /api/projects
projects.POST("", handler)
// handle GET /api/projects/:id
projects.GET("/:id", handler)
Expand Methods

ExpandMethods sets up 405 Method Not Allowed handlers for methods on routes that don't have a handler yet.

router := jug.New()
router.GET("/api/users", ...)
router.POST("/api/users", ...)
router.ExpandMethods()
GET /api/users    -> 200
POST /api/users   -> 201
PUT /api/users    -> 405
DELETE /api/users -> 405
GET /foo/bar      -> 404
Reading Path Parameters
router := jug.New()

router.GET("/api/users/:userId", func(c jug.Context) {
	userId := c.Param("userId")
	c.String(http.StatusOK, "user: %s", userId)
})
Reading Query Parameters
func query(c jug.Context) {
	
	rawValue := c.Query(key)
	
	sliceValue := c.QueryArray(key)
	
	intValue, err := c.IntQuery(key)
	
	boolValue, err := c.BoolQuery(key)
	
	dateValue, err := c.Iso8601DateQuery(key)
	
	dateTimeValue, err := c.Iso8601DateTimeQuery(key)
	
	stringValue, err := c.StringQuery(key)
	
	valueOrDefault := c.DefaultQuery(key, defaultValue)
	
	intValueOrDefault, err := c.DefaultIntQuery(key, defaultValue)
	
	boolValueOrDefault, err := c.DefaultBoolQuery(key, defaultValue)
	
	stringValueOrDefault, err := c.DefaultStringQuery(key, defaultValue)
}
Reading Headers
func headers(c jug.Context) {
    headerValue := c.GetHeader(headerName)
}
Reading Request Body
func rawData(c jug.Context) {
    // get the raw request body
    data, err := c.GetRawData()
}

func mayBindJSON(c jug.Context) {
    var query Query
    if c.MayBindJSON(&query) {
        c.RespondOk(query)
        return
    }
    c.String(http.StatusOk, "No request body given")
}

func mustBindJSON(c jug.Context) {
    var query Query

    // MustBindJSON responds 400 if the binding fails
    if !c.MustBindJSON(&query) {
        return
    }
    c.RespondOk(query)
}

Validating Input

Use the Validator to validate data.

message := "Hello World"

err := jug.NewValidator().
	RequireStringnotEmpty(message, "message is required").
	Validate()

if err != nil {
	log.Println("invalid message", err)
}

Types that implement the Validatable interface are automatically validated when bound from JSON.

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

func (r CreateUserRequest) Validate() error {
	return jug.NewValidator().
		RequireStringNotEmpty(r.Name, "name is required").
		RequireStringMinLength(r.Name, 2, "name needs at least 2 characters").
		Validate()
}

handler := func(c jug.Context) {
	var req CreateUserRequest
	if !c.MustBindJSON(&req) {
		// if binding or validation fails an appropriate response is generated
		return
    }
	c.RespondCreated(req)
}

If you need more control over the validation process you can use the *V functions and provide a validator function your own. This is useful if you need to access contextual information during the validation.

type CreateUserRequest struct {
	Name string `json:"name"`
	Age int `json:"age"`
	Role string `json:"role""`
}

handler := func(c jug.Context) {
	var req CreateUserRequest
	if !c.MustBindJSONV(&req, func()error {
        return jug.NewValidator().
        RequireEnum(r.Role, loadRolesFromDatabase(), "invalid role")
        Validate()
    }) {
		// if binding or validation fails an appropriate response is generated
		return
    }
	c.RespondCreated(req)
}
Simple Responses

Response methods that take a response body argument marshal the given object to JSON unless specified otherwise.

var c jug.Context

c.Data(statusCode int, contentType string, data []byte)

c.String(statusCode int, format, args...)

c.RespondOk(responseBody any)

c.RespondNoContent()

c.RespondCreated(responseBody any)

c.RespondForbidden(responseBody any)

c.RespondUnauthorized(responseBody any)
c.RespondUnauthorizedE(err error)

c.RespondBadRequest(responseBody any)
c.RespondBadRequestE(err error)

c.RespondNotFound(responseBody any)
c.RedpondNotFoundE(err error)

c.RespondConflict(responseBody any)
c.RespondConflictE(err error)

c.RespondInternalServerError(responseBody any)
c.RespondInternalServerErrorE(err error)

c.RespondMissingRequestBody()
Streaming Responses

Create streaming responses using the Stream method. It takes a step function that is provided with a writer. Use the writer to write to the response. Return true to keep the response open. The step function will be called again. Return false to end the response and close the connection.

router.GET("/api/stream", func(c jug.Context) {
	i := 10
	c.Stream(func(w io.Writer) bool {
        if i > 0 {
            _, _ = fmt.Fprintf(w, "Remaining: %d\n", i)
            i = i -1
            time.Sleep(time.Second)
            return true
		}
	    return false
    })
})
Server Sent Events

To emit server sent events, use SSEvent inside a Stream step function.

router.GET("/api/sse", func(c jug.Context) {
    clientChan := make(chan string)
    go func() {
        data := fetchDataWhichTakesSomeTime()
        clientChan <- data
        close(clientChan)
    }()

    c.Stream(func(w io.Writer) bool {
        if msg, ok := <-clientChan; ok {
            c.SSEvent("message", msg)
            return true
        }
        return false
    })
})
Cookies

Getting cookies:

func getCookie(c jug.Context) {
    cookie, err := c.Cookie("my-cookie")
    if err != nil {
        if errors.is(err, http.ErrNoCookie) {
            log.Println("no cookie in request")
            return
        }
        log.Fatal(err)
    }
    log.Println("cookie value", cookie)
}

Setting cookies:

func setCookie(c jug.Context) {
	maxAge := 0
	path := ""
	domain := ""
	secure := false
	httOnly := true
	c.SetCookie("my-cookie", "cookies be great", maxAge, path, domain, secure, httpOnly)
}
Using Middleware

Middleware are global handlers that will be executed for every single request. Typical use cases are loggers, authentication filters, error handlers etc.

A middleware can be registered at the root level or for a route. It will be executed for all child routes too.

func middleware(c jug.Context) {
	log.Println("hello from the middleware")
}

router := jug.New()
router.Use(middleware)
router.GET("/api/users", handler)
router.GET("/api/projects", handler)

GET /api/users ->  200, hello from the middleware
GET /api/projects -> 200, hello from the middleware
GET /foo/bar -> 404, hello from the middleware

router := jug.New()

usersGroup := router.Group("/api/users", middleware)
usersGroup.GET("", handler)
usersGroup.GET("/:id", handler)
router.GET("/api/projects", handler)

GET / -> 404
GET /api/users -> 200, hello from the middleware
GET /api/users/3 -> 200, hello from the middleware
GET /api/useres/3/foo -> 404, hello from the middleware
GET /api/projects -> 200
Using the Context

Each client request has its own context. Handlers can set and get data to and from the context.

middleware := func(c jug.Context) {
    requestId := c.GetHeader("RequestId")
    if len(requestId) > 0 {
        c.Set("requestId", requestId)
    }
}

handler := func(c jug.Context) {
    requestId, ok := c.Get("requestId")
    if ok {
        c.String(http.StatusOK, "Request id: %s", requestId.(string))
    } else {
        c.String(http.StatusOK, "No Request id given")
    }
}
Handling Errors

Use HandleError to inspect an error and to write an appropriate response.

func (c jug.Context) {
	data, err := loadDataFromDatabase()
	if err != nil {
		c.HandleError(err)
		return
    }
	c.RespondOk(data)
}

HandleError sets appropriate status codes and response messages for:

err := jug.NewResponseStatusError(statusCode, message)

err := jug.NewBadRequestError(message)

err := jug.NewUnauthorizedError(message)

err := jug.NewForbiddenError(message)

err := jug.NewConflictError(message)

Unsupported errors lead to an HTTP 500 response.

Debug Mode

Enables the gin debug mode.

router := jug.New()
router.EnableDebugMode()

Documentation

Index

Constants

This section is empty.

Variables

This section is empty.

Functions

func MapMany

func MapMany[T any, R any](x []T, m func(e T) R) []R

func MapManyE

func MapManyE[T any, R any](x []T, m func(e T) (R, error)) ([]R, error)

func MethodNotAllowed

func MethodNotAllowed(c Context)

Types

type Context

type Context interface {
	// Get gets a value from the context.
	Get(key string) (any, bool)
	// MustGet tries to get a value from the context. If the value cannot be found the request is aborted.
	MustGet(key string) any
	// Set sets a context value.
	Set(key string, value any)

	// Query gets a raw query value
	Query(key string) string
	// QueryArray gets an array query value
	QueryArray(key string) []string
	// IntQuery gets a query value as int
	IntQuery(key string) (int, error)
	// BoolQuery gets a query value as bool
	BoolQuery(key string) (bool, error)
	// Iso8601DateQuery gets a query value as ISO 8601 Date
	Iso8601DateQuery(key string) (*time.Time, error)
	// Iso8601DateTimeQuery gets a query values as ISO 8601 DateTime
	Iso8601DateTimeQuery(key string) (*time.Time, error)
	// StringQuery gets a query value as string. This method performs unescaping.
	StringQuery(key string) (string, error)
	// DefaultQuery gets a query value. If the value cannot be found a default value is returned.
	DefaultQuery(key string, defaultValue string) string
	// DefaultIntQuery gets a query value as int. If the value cannot be found a default value is returned.
	DefaultIntQuery(key string, defaultValue int) (int, error)
	// DefaultBoolQuery gets a query value as bool. If the value cannot be found a default value is returned.
	DefaultBoolQuery(key string, defaultValue bool) (bool, error)
	// DefaultStringQuery gets a query value as string. If the value cannot be found a default value is returned.
	DefaultStringQuery(key string, defaultValue string) (string, error)
	// GetHeader gets a request header
	GetHeader(key string) string

	// Param gets a request param (aka path parameter)
	Param(key string) string

	// GetRawData gets the raw request body
	GetRawData() ([]byte, error)
	// MayBindJSON tries to bind the request body from JSON to the given object.
	MayBindJSON(obj any) bool
	// MayBindJSONV tries to bind the request body from JSON to the given object. It will then invoke the provided validator function.
	MayBindJSONV(obj any, validator func() error) bool
	// MustBindJSON tries to bind the request body from JSON to the given object. If that fails the request is aborted with 400.
	MustBindJSON(obj any) bool
	// MustBindJSONV tries to bind the request body from JSON to the given object. If that fails the request is aborted with 400.
	// If it succeeds the provided validator function is invoked.
	MustBindJSONV(obj any, validator func() error) bool

	// Request gets the original http request
	Request() *http.Request
	// Writer gets the http response writer
	Writer() http.ResponseWriter
	//ClientIP implements one best effort algorithm to return the real client IP.
	//It calls c.RemoteIP() under the hood, to check if the remote IP is a trusted proxy or not.
	//If it is it will then try to parse the headers defined in Engine.RemoteIPHeaders (defaulting to [X-Forwarded-For, X-Real-Ip]).
	//If the headers are not syntactically valid OR the remote IP does not correspond to a trusted proxy,
	//the remote IP (coming from Request.RemoteAddr) is returned.
	ClientIP() string
	//RemoteIP parses the IP from Request.RemoteAddr, normalizes and returns the IP (without the port).
	RemoteIP() string

	// Status sets the response status code.
	Status(code int) Context
	// String sets the response status code and writes a string response.
	String(code int, format string, values ...any) Context

	// SetHeader sets a response header.
	SetHeader(key string, value string)
	// SetContentType sets the response content type.
	SetContentType(value string)

	// Cookie returns the named cookie provided in the request or false if not found.
	// If multiple cookies match the given name, only one cookie will be returned.
	Cookie(name string) (string, bool)
	// SetCookie sets a cookie.
	SetCookie(name string, value string, maxAge int, path string, domain string, secure bool, httpOnly bool)

	// Stream writes a stream response.
	Stream(step func(w io.Writer) bool) bool
	// SSEvent writes a server sent event.
	SSEvent(name string, message any)

	// Data sets the response status code and writes the given data as is.
	Data(code int, contentType string, data []byte)

	// RespondOk sets status 200, marshals obj to JSON
	RespondOk(obj any)
	// RespondNoContent sets status 204, no response body
	RespondNoContent()
	// RespondCreated sets status 201, marshals obj to JSON
	RespondCreated(obj any)
	// RespondForbidden sets status 403, marshals obj to JSON
	RespondForbidden(obj any)
	// RespondForbiddenE sets status 403, writes error as error response (JSON)
	RespondForbiddenE(err error)
	// RespondUnauthorized sets status 401, marshals obj to JSON
	RespondUnauthorized(obj any)
	// RespondUnauthorizedE sets status 401, writes error as error response (JSON)
	RespondUnauthorizedE(err error)
	// RespondBadRequest sets status 400, marshals obj to JSON
	RespondBadRequest(obj any)
	// RespondBadRequestE sets status 400, writes error as error response (JSON)
	RespondBadRequestE(err error)
	// RespondNotFound sets status 404, marshals obj to JSON
	RespondNotFound(obj any)
	// RespondNotFoundE sets status 404, writes error as error response (JSON)
	RespondNotFoundE(err error)
	// RespondConflict sets status 409, marshals obj to JSON
	RespondConflict(obj any)
	// RespondConflictE sets status 409, writes error as error response (JSON)
	RespondConflictE(err error)
	// RespondInternalServerError sets status 500, marshals obj to JSON
	RespondInternalServerError(obj any)
	// RespondInternalServerErrorE sets status 500, writes error as error response (JSON)
	RespondInternalServerErrorE(err error)

	// RespondMissingRequestBody sets status 400, writes error response (JSON)
	RespondMissingRequestBody()

	// Abort prevents pending handlers being called. This will not stop the current handler.
	Abort()
	// AbortWithError prevents pending handlers being called. This will not stop the current handler.
	// The response status code is set to the given value.
	// Writes error as error response (JSON).
	AbortWithError(code int, err error)

	// Next should only be used in middlewares. It executes pending handlers in the chain inside the current handler.
	Next()

	// HandleError inspects the given error and writes an appropriate response.
	HandleError(err error)

	// Deadline returns that there is no deadline (ok==false) when c.Request has no Context.
	Deadline() (deadline time.Time, ok bool)
	// Done returns nil (chan which will wait forever) when c.Request has no Context.
	Done() <-chan struct{}
	// Err returns nil when c.Request has no Context.
	Err() error
	// Value returns the value associated with this context for key, or nil if no value is associated with key.
	// Successive calls to Value with the same key returns the same result.
	Value(key any) any
}

type Engine

type Engine interface {
	RouterGroup

	NoMethod(handlers ...HandlerFunc)
	NoRoute(handlers ...HandlerFunc)

	// ExpandMethods expands each non-configured method for each path to return 405 Method not allowed
	ExpandMethods()

	Run(addr ...string) error

	EnableDebugMode()

	ServeHTTP(w http.ResponseWriter, req *http.Request)
}

func Default

func Default() Engine

func New

func New() Engine

type HandlerFunc

type HandlerFunc func(c Context)

type PathRegistry

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

func NewPathRegistry

func NewPathRegistry() *PathRegistry

func (*PathRegistry) Add

func (p *PathRegistry) Add(path string, methods ...string)

func (*PathRegistry) Get

func (p *PathRegistry) Get(path string, method string) bool

func (*PathRegistry) Paths

func (p *PathRegistry) Paths() []string

type ResponseStatusError

type ResponseStatusError struct {
	StatusCode int
	Message    string
}

func NewBadRequestError

func NewBadRequestError(message string) *ResponseStatusError

func NewConflictError

func NewConflictError(message string) *ResponseStatusError

func NewForbiddenError

func NewForbiddenError(message string) *ResponseStatusError

func NewNotFoundError

func NewNotFoundError(message string) *ResponseStatusError

func NewResponseStatusError

func NewResponseStatusError(statusCode int, message string) *ResponseStatusError

func NewUnauthorizedError

func NewUnauthorizedError(message string) *ResponseStatusError

func (*ResponseStatusError) Error

func (e *ResponseStatusError) Error() string

type Router

type Router interface {
	Use(middleware ...HandlerFunc) Router
	Any(relativePath string, handlers ...HandlerFunc) Router
	GET(relativePath string, handlers ...HandlerFunc) Router
	POST(relativePath string, handlers ...HandlerFunc) Router
	PUT(relativePath string, handlers ...HandlerFunc) Router
	DELETE(relativePath string, handlers ...HandlerFunc) Router
	PATCH(relativePath string, handlers ...HandlerFunc) Router
	OPTIONS(relativePath string, handlers ...HandlerFunc) Router
	HEAD(relativePath string, handlers ...HandlerFunc) Router
}

type RouterGroup

type RouterGroup interface {
	Router
	Group(relativePath string, handlers ...HandlerFunc) RouterGroup
}

type Validatable

type Validatable interface {
	Validate() error
}

type Validator

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

func NewValidator

func NewValidator() *Validator

func ValidateSub

func ValidateSub[T Validatable](v *Validator, key string, items []T) *Validator

ValidateSub performs validation on a sub item.

func (*Validator) Require

func (v *Validator) Require(condition bool, message string) *Validator

Require requires a condition to be truthy

func (*Validator) RequireEnum

func (v *Validator) RequireEnum(s string, message string, values ...string) *Validator

RequireEnum requires a value to be found in a given enum

func (*Validator) RequireMatchesRegex

func (v *Validator) RequireMatchesRegex(s string, regex *regexp.Regexp, message string) *Validator

RequireMatchesRegex requires a value to match a given regular expression

func (*Validator) RequireMaxLength added in v0.2.0

func (v *Validator) RequireMaxLength(s string, max int, message string) *Validator

RequireMaxLength requires a value to have a given maximum length

func (*Validator) RequireMinLength added in v0.2.0

func (v *Validator) RequireMinLength(s string, min int, message string) *Validator

RequireMinLength requires a value to have a given minimum length

func (*Validator) RequireNotEmpty added in v0.2.0

func (v *Validator) RequireNotEmpty(s string, message string) *Validator

RequireNotEmpty requires a string not to be empty

func (*Validator) RequireSliceEnum added in v0.2.0

func (v *Validator) RequireSliceEnum(s []string, message string, values ...string) *Validator

RequireSliceEnum requires the given slice to only contain elements from values...

func (*Validator) RequireSliceMinLength added in v0.2.0

func (v *Validator) RequireSliceMinLength(s []string, min int, message string) *Validator

RequireSliceMinLength requires the given slice to have at least min elements

func (*Validator) RequireSliceNotEmpty added in v0.2.0

func (v *Validator) RequireSliceNotEmpty(s []string, message string) *Validator

RequireSliceNotEmpty requires the given slice not to be empty

func (*Validator) RequireStringLengthBetween

func (v *Validator) RequireStringLengthBetween(s string, min int, max int, message string) *Validator

RequireStringLengthBetween requires a string TODO

func (*Validator) RequireStringMaxLength deprecated

func (v *Validator) RequireStringMaxLength(s string, max int, message string) *Validator

RequireStringMaxLength requires a value to have a given maximum length

Deprecated: use RequireMaxLength instead.

func (*Validator) RequireStringMinLength deprecated

func (v *Validator) RequireStringMinLength(s string, min int, message string) *Validator

RequireStringMinLength requires a value to have a given minimum length

Deprecated: use RequireMinLength instead.

func (*Validator) RequireStringNotEmpty deprecated

func (v *Validator) RequireStringNotEmpty(s string, message string) *Validator

RequireStringNotEmpty requires a string not to be empty

Deprecated: use RequireNotEmpty instead

func (*Validator) RequireStringSliceEnum deprecated

func (v *Validator) RequireStringSliceEnum(s []string, message string, values ...string) *Validator

RequireStringSliceEnum requires the given slice to only contain elements from values...

Deprecated: use RequireSliceEnum instead.

func (*Validator) RequireStringSliceMinLength deprecated

func (v *Validator) RequireStringSliceMinLength(s []string, min int, message string) *Validator

RequireStringSliceMinLength requires the given slice to have at least min elements

Deprecated: use RequireSliceMinLength instead.

func (*Validator) RequireStringSliceNotEmpty deprecated

func (v *Validator) RequireStringSliceNotEmpty(s []string, message string) *Validator

RequireStringSliceNotEmpty requires the given slice not to be empty

Deprecated: use RequireSliceNotEmpty instead.

func (*Validator) V

func (v *Validator) V(fun func(*Validator)) *Validator

V invokes a validation function on the validator

func (*Validator) Validate

func (v *Validator) Validate() error

Validate performs the validation

Jump to

Keyboard shortcuts

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