cmux

package module
v0.0.0-...-f84a75e Latest Latest
Warning

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

Go to latest
Published: Apr 22, 2024 License: GPL-3.0 Imports: 17 Imported by: 0

README

cmux

HTTP router for creating JSON APIs in Golang. Featuring built-in JSON handling and typesafe path variables etc.

Basic HTTP Request Handling

Handle methods like Get or Post. Methods with a request body like POST, must specify the data structure which the JSON should be encoded to, as a function parameter (e.g. cmux.Request[cmux.SomeRequestBody, *Metadatastruct] error {...). This data is then accessed from the Body field of cmux.Request.

func main() {
    m := cmux.Mux{}
    type PostData struct{
        SomeValue string `json:"some_value"`
    }
    type Md struct{}
    m.HandleFunc("/", &Md{},
        cmux.Get(func(req *cmux.Request[cmux.EmptyBody, *Md]) error {
            return nil
        }, nil),
        cmux.Post(func(req *cmux.Request[PostData, *Md]) error {
            fmt.Println("Received post value:", req.Body.SomeValue)
            return nil
        }, nil),
    )
    http.ListenAndServe("localhost:8080", &m)
}

Using Path Variables

Define path variables using curly brackets in the path and retrieve values by passing a struct to HandleFunc. The field tag "cmux" can be used to specify which path variable the field represents. Alternately path variables are saved to field names matching the path variable (case-insensitive). Path variables can have prefixes or suffixes. Note only one variable is supported per path section (i.e. between a pair of '/').

func main() {
    m := cmux.Mux{}
    type Md struct{
        City       string
        StreetName string `cmux:"street"`
    }
    m.HandleFunc("/city-{city}/{street}", &Md{},
        cmux.Get(func(req *cmux.Request[cmux.EmptyBody, *Md]) error {
            fmt.Println("city:", req.Metadata.City, "street:", req.Metadata.StreetName)
            return nil
        }, nil),
    )
    http.ListenAndServe("localhost:8080", &m)
}

Responding

When a MethodHandler returns a type that implements the HTTPResponder interface (and the error interface), the HTTPRespond method is called and the response is encoded as JSON (unless an error is returned).

type Md struct {}

type Cake struct {
    Name            string `json:"name"`
    StrawberryCount uint   `json:"strawberry_count"`
}

func (c *Cake) HTTPRespond() (any, error) {
    return c, nil
}

func (c *Cake) Error() string {
    return "not filtered"
}

func GetCake(req *cmux.Request[cmux.EmptyBody, *Md]) error {
    return &Cake{
        Name:           "Large Strawberry Cake",
        StrawberryCount: 5,
    }
}

func main() {
    m := cmux.Mux{}
    m.HandleFunc("/get-cake", &Md{},
        cmux.Get(GetCake, nil),
    )
    http.ListenAndServe("localhost:8080", &m)
}
Bypass

A type implementing the HTTP Response interface and the error interface can be returned directly in Methodhandler functions. This can be bypassed by using the mux.Bypass.

type Md struct {}

type Cake struct {
    Name            string `json:"name"`
    StrawberryCount uint   `json:"strawberry_count"`
}

func GetCake(req *cmux.Request[cmux.EmptyBody, *Md]) error {
    return cmux.Bypass(&Cake{
        Name:           "Large Strawberry Cake",
        StrawberryCount: 5,
    })
}

func main() {
    m := cmux.Mux{}
    m.HandleFunc("/get-cake", &Md{},
        cmux.Get(GetCake, nil),
    )
    http.ListenAndServe("localhost:8080", &m)
}
Filter a response

HTTPRespond can also filter secret fields. This is useful when loading JSON documents from a database that contains fields that must not be publically available. In turn this allows the use of the same data structures.

type ResData struct {
    PublicData  string `json:"public_data"`
    PrivateData string `json:"private_data,omitempty"`
}

func (r *ResData) HTTPRespond() (any, error) {
    return &ResData{
        PublicData: r.PublicData,
    }, nil
}

func (r *ResData) Error() string {
    return "not filtered"
}

func main() {
    m := cmux.Mux{}
    type Md struct{}
    m.HandleFunc("/info", &Md{},
        cmux.Get(func(req *cmux.Request[cmux.EmptyBody, *Md]) error {
            return &ResData{
                PublicData: "some public data",
                PrivateData: "some private data",
            }
        }, nil),
    )
    http.ListenAndServe("localhost:8080", &m)
}
Transform a response
type CreditCard struct {
    CardNumber string `json:"card_number"`
}

func (cc *CreditCard) HTTPRespond() (any, error) {
    var cardType string
    if strings.HasPrefix(cc.CardNumber, "4") {
        cardType = "visa"
    } else if strings.HasPrefix(cc.CardNumber, "5") {
        cardType = "mastercard"
    } else {
        return nil, cmux.HTTPError("unknown card type", 400)
    }
    return struct {
        CardType string `json:"card_type"`
    }{
        CardType: cardType,
    }, nil
}

func (r *CreditCard) Error() string {
    return "not filtered"
}

func main() {
    m := cmux.Mux{}
    type Md struct{}
    m.HandleFunc("/identify-card", &Md{},
        cmux.Post(func(req *cmux.Request[CreditCard, *Md]) error {
            return &req.Body
        }, nil),
    )
    http.ListenAndServe("localhost:8080", &m)
}

Returning errors

HTTP errors can be returned directly using cmux.HTTPError(err string, code int) error or cmux.WrapError(err error, code int) error or by returning a type satisfying the HTTPErrorResponder interface.

type CustomError struct{}

func (ce *CustomError) HTTPError()(int, any) {
    return 400, struct{
        AlternateError string `json:"alternate_error"`
    }{
        AlternateError: "not good",
    }
}

func (ce *CustomError) Error() string {
    return "did not respond correctly to error"
}

func main() {
    m := cmux.Mux{}
    type PostData struct{
        SomeValue string `json:"some_value"`
    }
    type Md struct{}
    m.HandleFunc("/", &Md{},
        cmux.Get(func(req *cmux.Request[cmux.EmptyBody, *Md]) error {
            return cmux.HTTPError("", http.StatusNotFound)
        }, nil),
        cmux.Post(func(wreq *cmux.Request[PostData, *Md]) error {
            return cmux.WrapError(errors.New("something bad happened"), http.StatusInternalServerError)
        }, nil),
        cmux.Put(func(req *cmux.Request[PostData, *Md]) error {
            return &CustomError{}
        }, nil),
    )
    http.ListenAndServe("localhost:8080", &m)
}

Before and Method Handler Data

Each Method Handler can be passed a custom data argument, which is processed by the Mux's Before method. This could be a simple string for access control list or permissions handling or a struct containing more complex data. Here we require requests to ("http://localhost:8080/cities/{city}") to have pass a "{city}_mayor" token in the Token HTTP request header:

func main() {
    type Md struct{
        City string
    }
    m := cmux.Mux{
        Before: func(req *http.Request, metadata, methodData any) error {
            switch v := metadata.(type) {
            case *Md:
                permission, ok := methodData.(string)
                if !ok {
                    panic("expected a permission string, got something else")
                }
                if req.Header.Get("token") != v.City + "_" + permission {
                    return cmux.HTTPError("", http.StatusForbidden)
                }
            default:
                return fmt.Errorf("unexpected metadata type %T", v)
            }
            return nil
        },
    }
    m.HandleFunc("/cities/{city}", &Md{},
        cmux.Get(func(req *cmux.Request[cmux.EmptyBody, *Md]) error {
            return nil
        }, "mayor"),
    )
    http.ListenAndServe("localhost:8080", &m)
}

Performing the following curl command in a terminal will then yield a 200 response code:

curl -v localhost:8080/cities/london -H 'Token: london_mayor'

Documentation

Index

Constants

This section is empty.

Variables

View Source
var DefaultMux = &Mux{}

Functions

func HTTPError

func HTTPError(err string, code int) error

HTTPError creates an error that when returned in a MethodHandler makes the server reply with the specified error message and HTTP code.

func HandleFunc

func HandleFunc(path string, metadata any, mhs ...MethodHandler)

func WrapError

func WrapError(err error, code int) error

HTTPError creates an error that when returned in a MethodHandler makes the server reply with the specified error and HTTP code.

Types

type BypassingData

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

Bypassed data can be returned directly in method handler functions.

func Bypass

func Bypass(res any) BypassingData

Bypass takes any data and transforms it into data that can be directly returned in method handler functions.

func (BypassingData) Error

func (wd BypassingData) Error() string

func (BypassingData) HTTPRespond

func (wd BypassingData) HTTPRespond() (any, error)

type EmptyBody

type EmptyBody struct{}

type HTTPErrorResponder

type HTTPErrorResponder interface {
	HTTPError() (int, any)
}

Returning an error that also implements HTTPErrorResponder in a MethodHandler function will cause the server to call the HTTPError method and respond with the specified int as status code and JSON-encode the 'any' value as the body.

type HTTPResponder

type HTTPResponder interface {
	HTTPRespond() (any, error)
}

Returning an error that also implements HTTPResponder in a MethodHandler function will cause the server to call HTTPRespond and respond to the incoming request with the returned values. If the error is non-nil the response will be an error. If the error is nil, the response will be the JSON-encoded 'any' return value.

type MethodHandler

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

MethodHandlers each handles a specific HTTP Method. They are returned by the functions Delete, Get, Head, Options, Patch, Post, Put, Trace.

func Delete

func Delete[I EmptyBody, M any](fn func(*Request[I, M]) error, data any) MethodHandler

Handle DELETE HTTP method requests.

func Get

func Get[I EmptyBody, M any](fn func(*Request[I, M]) error, data any) MethodHandler

Handle GET HTTP method requests.

func Head[I EmptyBody, M any](fn func(*Request[I, M]) error, data any) MethodHandler

Handle HEAD HTTP method requests.

func Options

func Options[I EmptyBody, M any](fn func(*Request[I, M]) error, data any) MethodHandler

Handle OPTIONS HTTP method requests.

func Patch

func Patch[I any, M any](fn func(*Request[I, M]) error, data any) MethodHandler

Handle PATCH HTTP method requests.

func Post

func Post[I any, M any](fn func(*Request[I, M]) error, data any) MethodHandler

Handle POST HTTP method requests.

func Put

func Put[I any, M any](fn func(*Request[I, M]) error, data any) MethodHandler

Handle PUT HTTP method requests.

func Trace

func Trace[I EmptyBody, M any](fn func(*Request[I, M]) error, data any) MethodHandler

Handle TRACE HTTP method requests.

type Mux

type Mux struct {
	Before func(http.ResponseWriter, *http.Request, any, any) error
	// contains filtered or unexported fields
}

func (*Mux) EnableDebug

func (mux *Mux) EnableDebug(enable bool)

func (*Mux) EnableDebugTimings

func (mux *Mux) EnableDebugTimings(enable bool)

func (*Mux) HandleFunc

func (mux *Mux) HandleFunc(path string, metadata any, mhs ...MethodHandler)

HandleFunc handles requests matching the specified path in the speciified MethodHandlers. The metadata is copied for each new incoming request and can be mutated by the Mux.Before method before being available in the MethodHandler functions.

func (*Mux) Print

func (mux *Mux) Print(w io.Writer, indent string)

func (*Mux) ServeHTTP

func (mux *Mux) ServeHTTP(w http.ResponseWriter, r *http.Request)

func (*Mux) SetDefaultContentType

func (mux *Mux) SetDefaultContentType(ctype string)

type PathParser

type PathParser interface {
	ParsePath()
}

type Request

type Request[T any, M any] struct {
	Body     T
	Metadata M

	/* Underlying native golang request / responsewriter: */
	HTTPReq        *http.Request
	ResponseWriter http.ResponseWriter
}

Request stores incoming request data. Body contains the unmarshaled body of the request Metadata contains custom data that is passed to the HandleFunc and can be mutated by the mux Before Method. It also provides access to the underlying http.Request and http.ResponseWriter

Jump to

Keyboard shortcuts

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