Categorygithub.com/cblach/cmux
repositorypackage
0.0.0-20240628073441-2c5f6f75d91d
Repository: https://github.com/cblach/cmux.git
Documentation: pkg.go.dev

# 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'