Documentation ¶
Overview ¶
Package hrt implements a type-safe HTTP router. It aids in creating a uniform API interface while making it easier to create API handlers.
Example (Get) ¶
package main import ( "context" "fmt" "net/url" "strings" "github.com/go-chi/chi/v5" "github.com/pkg/errors" "libdb.so/hrt" "libdb.so/hrt/internal/ht" ) // EchoRequest is a simple request type that echoes the request. type EchoRequest struct { What string `query:"what"` } // Validate implements the hrt.Validator interface. func (r EchoRequest) Validate() error { if !strings.HasSuffix(r.What, "!") { return errors.New("enthusiasm required") } return nil } // EchoResponse is a simple response that follows after EchoRequest. type EchoResponse struct { What string `json:"what"` } func handleEcho(ctx context.Context, req EchoRequest) (EchoResponse, error) { return EchoResponse{What: req.What}, nil } func main() { r := chi.NewRouter() r.Use(hrt.Use(hrt.DefaultOpts)) r.Get("/echo", hrt.Wrap(handleEcho)) srv := ht.NewServer(r) defer srv.Close() resp := srv.MustGet("/echo", url.Values{"what": {"hi"}}) fmt.Printf("HTTP %d: %s", resp.Status, resp.Body) resp = srv.MustGet("/echo", url.Values{"what": {"hi!"}}) fmt.Printf("HTTP %d: %s", resp.Status, resp.Body) }
Output: HTTP 400: {"error":"400: enthusiasm required"} HTTP 200: {"what":"hi!"}
Example (Post) ¶
package main import ( "context" "fmt" "sync" "github.com/go-chi/chi/v5" "github.com/pkg/errors" "libdb.so/hrt" "libdb.so/hrt/internal/ht" ) // User is a simple user type. type User struct { ID int `json:"id"` Name string `json:"name"` } var ( users = make(map[int]User) usersMu sync.RWMutex ) // GetUserRequest is a request that fetches a user by ID. type GetUserRequest struct { ID int `url:"id"` } // Validate implements the hrt.Validator interface. func (r GetUserRequest) Validate() error { if r.ID == 0 { return errors.New("invalid ID") } return nil } func handleGetUser(ctx context.Context, req GetUserRequest) (User, error) { usersMu.RLock() defer usersMu.RUnlock() user, ok := users[req.ID] if !ok { return User{}, hrt.WrapHTTPError(404, errors.New("user not found")) } return user, nil } // CreateUserRequest is a request that creates a user. type CreateUserRequest struct { Name string `json:"name"` } // Validate implements the hrt.Validator interface. func (r CreateUserRequest) Validate() error { if r.Name == "" { return errors.New("name is required") } return nil } func handleCreateUser(ctx context.Context, req CreateUserRequest) (User, error) { user := User{ ID: len(users) + 1, Name: req.Name, } usersMu.Lock() users[user.ID] = user usersMu.Unlock() return user, nil } func main() { r := chi.NewRouter() r.Use(hrt.Use(hrt.DefaultOpts)) r.Route("/users", func(r chi.Router) { r.Get("/{id}", hrt.Wrap(handleGetUser)) r.Post("/", hrt.Wrap(handleCreateUser)) }) srv := ht.NewServer(r) defer srv.Close() resps := []ht.Response{ srv.MustGet("/users/1", nil), srv.MustPost("/users", "application/json", ht.AsJSON(map[string]any{})), srv.MustPost("/users", "application/json", ht.AsJSON(map[string]any{ "name": "diamondburned", })), srv.MustGet("/users/1", nil), } for _, resp := range resps { fmt.Printf("HTTP %d: %s", resp.Status, resp.Body) } }
Output: HTTP 404: {"error":"404: user not found"} HTTP 400: {"error":"400: name is required"} HTTP 200: {"id":1,"name":"diamondburned"} HTTP 200: {"id":1,"name":"diamondburned"}
Index ¶
- Variables
- func ErrorHTTPStatus(err error, defaultCode int) int
- func RequestFromContext(ctx context.Context) *http.Request
- func Use(opts Opts) func(http.Handler) http.Handler
- func WithOpts(ctx context.Context, opts Opts) context.Context
- func Wrap[RequestT, ResponseT any](f func(ctx context.Context, req RequestT) (ResponseT, error)) http.HandlerFunc
- type CombinedEncoder
- type Decoder
- type Encoder
- type ErrorWriter
- type HTTPError
- type Handler
- type MethodDecoder
- type None
- type Opts
- type Validator
- type WriteErrorFunc
Examples ¶
Constants ¶
This section is empty.
Variables ¶
var DefaultEncoder = CombinedEncoder{ Encoder: EncoderWithValidator(JSONEncoder), Decoder: DecoderWithValidator(MethodDecoder{ "GET": URLDecoder, "*": JSONEncoder, }), }
DefaultEncoder is the default encoder used by the router. It decodes GET requests using the query string and URL parameter; everything else uses JSON.
For the sake of being RESTful, we use a URLDecoder for GET requests. Everything else will be decoded as JSON.
var DefaultOpts = Opts{ Encoder: DefaultEncoder, ErrorWriter: JSONErrorWriter("error"), }
DefaultOpts is the default options for the router.
var Empty = None{}
Empty is a value of None.
Functions ¶
func ErrorHTTPStatus ¶
ErrorHTTPStatus returns the HTTP status code for the given error. If the error is not an HTTPError, it returns defaultCode.
func RequestFromContext ¶
RequestFromContext returns the request from the Handler's context.
Types ¶
type CombinedEncoder ¶
CombinedEncoder combines an encoder and decoder pair into one.
func (CombinedEncoder) Decode ¶
func (e CombinedEncoder) Decode(r *http.Request, v any) error
Decode implements the Decoder interface.
func (CombinedEncoder) Encode ¶
func (e CombinedEncoder) Encode(w http.ResponseWriter, v any) error
Encode implements the Encoder interface.
type Decoder ¶
type Decoder interface { // Decode decodes the given value from the given reader. Decode(*http.Request, any) error }
Decoder describes a decoder that decodes the request type.
var URLDecoder Decoder = urlDecoder{}
URLDecoder decodes chi.URLParams and url.Values into a struct. It only does Decoding; the Encode method is a no-op. The decoder makes no effort to traverse the struct and decode nested structs. If neither a chi.URLParam nor a url.Value is found for a field, the field is left untouched.
The following tags are supported:
- `url` - uses chi.URLParam to decode the value.
- `form` - uses r.FormValue to decode the value.
- `query` - similar to `form`.
- `schema` - similar to `form`, exists for compatibility with gorilla/schema.
- `json` - uses either chi.URLParam or r.FormValue to decode the value. If the value is provided within the form, then it is unmarshaled as JSON into the field unless the type is a string. If the value is provided within the URL, then it is unmarshaled as a primitive value.
If a struct field has no tag, it is assumed to be the same as the field name. If a struct field has a tag, then only that tag is used.
Example ¶
The following Go type would be decoded to have 2 URL parameters:
type Data struct { ID string Num int `url:"num"` Nested struct { ID string } }
func DecoderWithValidator ¶
DecoderWithValidator wraps an encoder with one that calls Validate() on the value after decoding and before encoding if the value implements Validator.
type Encoder ¶
type Encoder interface { // Encode encodes the given value into the given writer. Encode(http.ResponseWriter, any) error // An encoder must be able to decode the same type it encodes. Decoder }
Encoder describes an encoder that encodes or decodes the request and response types.
var JSONEncoder Encoder = jsonEncoder{}
JSONEncoder is an encoder that encodes and decodes JSON.
func EncoderWithValidator ¶
EncoderWithValidator wraps an encoder with one that calls Validate() on the value after decoding and before encoding if the value implements Validator.
type ErrorWriter ¶
type ErrorWriter interface {
WriteError(w http.ResponseWriter, err error)
}
ErrorWriter is a writer that writes an error to the response.
var TextErrorWriter ErrorWriter = textErrorWriter{}
TextErrorWriter writes the error into the response in plain text. 500 status code is used by default.
func JSONErrorWriter ¶
func JSONErrorWriter(field string) ErrorWriter
JSONErrorWriter writes the error into the response in JSON. 500 status code is used by default. The given field is used as the key for the error message.
type HTTPError ¶
HTTPError extends the error interface with an HTTP status code.
func NewHTTPError ¶
NewHTTPError creates a new HTTPError with the given status code and message.
func OverrideHTTPError ¶
OverrideHTTPError overrides the HTTP status code of the given error. If the error is not of type HTTPError, it is wrapped with the given status code. If it is, the error is unwrapped and wrapped with the new status code.
func WrapHTTPError ¶
WrapHTTPError wraps an error with an HTTP status code. If the error is already of type HTTPError, it is returned as-is. To change the HTTP status code, use OverrideHTTPError.
type MethodDecoder ¶
MethodDecoder is an encoder that only encodes or decodes if the request method matches the methods in it.
type None ¶
type None struct{}
None indicates that the request has no body or the request does not return anything.
type Opts ¶
type Opts struct { Encoder Encoder ErrorWriter ErrorWriter }
Opts contains options for the router.
func OptsFromContext ¶
OptsFromContext returns the options from the Handler's context. DefaultOpts is returned if no options are found.
type Validator ¶
type Validator interface {
Validate() error
}
Validator describes a type that can validate itself.
type WriteErrorFunc ¶
type WriteErrorFunc func(w http.ResponseWriter, err error)
WriteErrorFunc is a function that implements the ErrorWriter interface.
func (WriteErrorFunc) WriteError ¶
func (f WriteErrorFunc) WriteError(w http.ResponseWriter, err error)
WriteError implements the ErrorWriter interface.