hrt

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

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

Go to latest
Published: Jun 19, 2024 License: ISC Imports: 10 Imported by: 7

README

Package hrt implements a type-safe HTTP router. It aids in creating a uniform API interface while making it easier to create API handlers.

HRT stands for (H)TTP (r)outer with (t)ypes.

Documentation

For documentation and examples, see GoDoc.

Dependencies

HRT depends on chi v5 for URL parameters when routing. Apps that use HRT should also use chi for routing.

Note that it is still possible to make a custom URL parameter decoder that would replace chi's, but it is not recommended.

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

Examples

Constants

This section is empty.

Variables

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.

View Source
var DefaultOpts = Opts{
	Encoder:     DefaultEncoder,
	ErrorWriter: JSONErrorWriter("error"),
}

DefaultOpts is the default options for the router.

View Source
var Empty = None{}

Empty is a value of None.

Functions

func ErrorHTTPStatus

func ErrorHTTPStatus(err error, defaultCode int) int

ErrorHTTPStatus returns the HTTP status code for the given error. If the error is not an HTTPError, it returns defaultCode.

func RequestFromContext

func RequestFromContext(ctx context.Context) *http.Request

RequestFromContext returns the request from the Handler's context.

func Use

func Use(opts Opts) func(http.Handler) http.Handler

Use creates a middleware that injects itself into each request's context.

func WithOpts

func WithOpts(ctx context.Context, opts Opts) context.Context

WithOpts returns a new context with the given options.

func Wrap

func Wrap[RequestT, ResponseT any](f func(ctx context.Context, req RequestT) (ResponseT, error)) http.HandlerFunc

Wrap wraps a handler into a http.Handler. It exists because Go's type inference doesn't work well with the Handler type.

Types

type CombinedEncoder

type CombinedEncoder struct {
	Encoder Encoder
	Decoder Decoder
}

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

func DecoderWithValidator(enc Decoder) Decoder

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

func EncoderWithValidator(enc Encoder) Encoder

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

type HTTPError interface {
	error
	HTTPStatus() int
}

HTTPError extends the error interface with an HTTP status code.

func NewHTTPError

func NewHTTPError(code int, str string) HTTPError

NewHTTPError creates a new HTTPError with the given status code and message.

func OverrideHTTPError

func OverrideHTTPError(code int, err error) HTTPError

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

func WrapHTTPError(code int, err error) HTTPError

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 Handler

type Handler[RequestT, ResponseT any] func(ctx context.Context, req RequestT) (ResponseT, error)

Handler describes a generic handler that takes in a type and returns a response.

func (Handler[RequestT, ResponseT]) ServeHTTP

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

ServeHTTP implements the http.Handler interface.

type MethodDecoder

type MethodDecoder map[string]Decoder

MethodDecoder is an encoder that only encodes or decodes if the request method matches the methods in it.

func (MethodDecoder) Decode

func (e MethodDecoder) Decode(r *http.Request, v any) error

Decode implements the Decoder interface.

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

func OptsFromContext(ctx context.Context) Opts

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.

Directories

Path Synopsis
internal
ht
Package ht contains HTTP testing utilities.
Package ht contains HTTP testing utilities.
rfutil
Package rfutil contains reflect utilities.
Package rfutil contains reflect utilities.

Jump to

Keyboard shortcuts

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