Skip to content

Commit

Permalink
docs: substantially improve documentation
Browse files Browse the repository at this point in the history
  • Loading branch information
kernle32dll committed Jul 25, 2024
1 parent f9601c4 commit 317846d
Show file tree
Hide file tree
Showing 15 changed files with 149 additions and 14 deletions.
18 changes: 18 additions & 0 deletions composition.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,13 +11,16 @@ import (
"time"
)

// GetEndpoint defines the contract for a ResourceHandler composition.
type GetEndpoint[T any] interface {
EntityUUID(r *http.Request) (string, error)
LastModification(ctx context.Context, entityUUID string) (time.Time, error)
FetchEntity(ctx context.Context, entityUUID string) (T, error)
HandleError(ctx context.Context, w http.ResponseWriter, r *http.Request, err error)
}

// ResourceHandler composes a full http.Handler for retrieving a single resource.
// This includes authentication, caching, and data retrieval.
func ResourceHandler[T any](
keySet jwk.Set,
getEndpoint GetEndpoint[T],
Expand All @@ -36,6 +39,7 @@ func ResourceHandler[T any](

// --------------------------

// GetSQLListEndpoint defines the contract for a ListSQLHandler composition.
type GetSQLListEndpoint[T any] interface {
ListHash(ctx context.Context, paging Paging) (string, error)
TotalCount(ctx context.Context) (uint, error)
Expand All @@ -44,6 +48,8 @@ type GetSQLListEndpoint[T any] interface {
HandleError(ctx context.Context, w http.ResponseWriter, r *http.Request, err error)
}

// ListSQLHandler composes a full http.Handler for retrieving a list of resources via SQL.
// This includes authentication, caching, and data retrieval.
func ListSQLHandler[T any](
keySet jwk.Set,
listEndpoint GetSQLListEndpoint[T],
Expand All @@ -62,6 +68,7 @@ func ListSQLHandler[T any](

// --------------------------

// GetSQLxListEndpoint defines the contract for a ListSQLxHandler composition.
type GetSQLxListEndpoint[T any] interface {
ListHash(ctx context.Context, paging Paging) (string, error)
TotalCount(ctx context.Context) (uint, error)
Expand All @@ -70,6 +77,8 @@ type GetSQLxListEndpoint[T any] interface {
HandleError(ctx context.Context, w http.ResponseWriter, r *http.Request, err error)
}

// ListSQLxHandler composes a full http.Handler for retrieving a list of resources via SQL.
// This includes authentication, caching, and data retrieval.
func ListSQLxHandler[T any](
keySet jwk.Set,
listEndpoint GetSQLxListEndpoint[T],
Expand All @@ -88,13 +97,16 @@ func ListSQLxHandler[T any](

// --------------------------

// GetStaticListEndpoint defines the contract for a StaticListHandler composition.
type GetStaticListEndpoint[T any] interface {
ListHash(ctx context.Context, paging Paging) (string, error)
TotalCount(ctx context.Context) (uint, error)
FetchEntities(ctx context.Context, paging Paging) ([]T, error)
HandleError(ctx context.Context, w http.ResponseWriter, r *http.Request, err error)
}

// StaticListHandler composes a full http.Handler for retrieving a list of resources from a static list.
// This includes authentication, caching, and data retrieval.
func StaticListHandler[T any](
keySet jwk.Set,
listEndpoint GetStaticListEndpoint[T],
Expand All @@ -113,12 +125,15 @@ func StaticListHandler[T any](

// --------------------------

// CreateEndpoint defines the contract for a ResourceCreateHandler composition.
type CreateEndpoint[T CreateDTO] interface {
EntityUUID(r *http.Request) (string, error)
CreateEntity(ctx context.Context, entityUUID, userUUID string, create T) error
HandleError(ctx context.Context, w http.ResponseWriter, r *http.Request, err error)
}

// ResourceCreateHandler composes a full http.Handler for creating a new resource.
// This includes authentication, and delegation of resource creation.
func ResourceCreateHandler[T CreateDTO](
keySet jwk.Set,
createEndpoint CreateEndpoint[T],
Expand All @@ -137,12 +152,15 @@ func ResourceCreateHandler[T CreateDTO](

// --------------------------

// PatchEndpoint defines the contract for a ResourcePatchHandler composition.
type PatchEndpoint[T PatchDTO] interface {
EntityUUID(r *http.Request) (string, error)
UpdateEntity(ctx context.Context, entityUUID, userUUID string, patch T, ifUnmodifiedSince time.Time) error
HandleError(ctx context.Context, w http.ResponseWriter, r *http.Request, err error)
}

// ResourcePatchHandler composes a full http.Handler for updating an existing resource.
// This includes authentication, and delegation of resource updating.
func ResourcePatchHandler[T PatchDTO](
keySet jwk.Set,
patchEndpoint PatchEndpoint[T],
Expand Down
5 changes: 3 additions & 2 deletions middleware_common.go
Original file line number Diff line number Diff line change
Expand Up @@ -60,7 +60,7 @@ var (
ErrReceivingMeta = errors.New("error while receiving metadata")

// ErrMissingUserUUID signals that a received JWT did not contain an user UUID.
ErrMissingUserUUID = errors.New("token does not include user uuid")
ErrMissingUserUUID = errors.New("token does not include user UUID")
)

type ResourceEntityFunc func(r *http.Request) (string, error)
Expand All @@ -78,6 +78,7 @@ func IsHandledByDefaultErrorHandler(err error) bool {
return errors.As(err, &validationErr)
}

// DefaultErrorHandler is a default error handler, which sensibly handles errors known by turtleware.
func DefaultErrorHandler(ctx context.Context, w http.ResponseWriter, r *http.Request, err error) {
if errors.Is(err, ErrResourceNotFound) {
WriteError(ctx, w, r, http.StatusNotFound, err)
Expand All @@ -98,7 +99,7 @@ func DefaultErrorHandler(ctx context.Context, w http.ResponseWriter, r *http.Req
WriteError(ctx, w, r, http.StatusInternalServerError, err)
}

// EntityUUIDMiddleware is a http middleware for extracting the uuid of the resource requested,
// EntityUUIDMiddleware is a http middleware for extracting the UUID of the resource requested,
// and passing it down.
func EntityUUIDMiddleware(entityFunc ResourceEntityFunc) func(h http.Handler) http.Handler {
return func(h http.Handler) http.Handler {
Expand Down
6 changes: 6 additions & 0 deletions middleware_create.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,10 @@ import (
"net/http"
)

// CreateFunc is a function called for delegating the handling of the creation of a new resource.
type CreateFunc[T CreateDTO] func(ctx context.Context, entityUUID, userUUID string, create T) error

// CreateDTO defines the contract for validating a DTO used for creating a new resource.
type CreateDTO interface {
Validate() []error
}
Expand All @@ -20,10 +22,14 @@ func IsHandledByDefaultCreateErrorHandler(err error) bool {
return IsHandledByDefaultErrorHandler(err)
}

// DefaultCreateErrorHandler is a default error handler, which sensibly handles errors known by turtleware.
func DefaultCreateErrorHandler(ctx context.Context, w http.ResponseWriter, r *http.Request, err error) {
DefaultErrorHandler(ctx, w, r, err)
}

// ResourceCreateMiddleware is a middleware for creating a new resource.
// It parses a turtleware.CreateDTO from the request body, validates it, and then calls the provided CreateFunc.
// Errors encountered during the process are passed to the provided turtleware.ErrorHandlerFunc.
func ResourceCreateMiddleware[T CreateDTO](createFunc CreateFunc[T], errorHandler ErrorHandlerFunc) func(http.Handler) http.Handler {
return func(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
Expand Down
32 changes: 32 additions & 0 deletions middleware_data.go
Original file line number Diff line number Diff line change
Expand Up @@ -13,14 +13,27 @@ import (
"os"
)

// ListStaticDataFunc is a function for retrieving a slice of data, scoped to the provided paging.
type ListStaticDataFunc[T any] func(ctx context.Context, paging Paging) ([]T, error)

// ListSQLDataFunc is a function for retrieving a sql.Rows iterator, scoped to the provided paging.
type ListSQLDataFunc func(ctx context.Context, paging Paging) (*sql.Rows, error)

// ListSQLxDataFunc is a function for retrieving a sqlx.Rows iterator, scoped to the provided paging.
type ListSQLxDataFunc func(ctx context.Context, paging Paging) (*sqlx.Rows, error)

// ResourceDataFunc is a function for retrieving a single resource via its UUID.
type ResourceDataFunc[T any] func(ctx context.Context, entityUUID string) (T, error)

// SQLResourceFunc is a function for scanning a single row from a sql.Rows iterator, and transforming it into a struct type.
type SQLResourceFunc[T any] func(ctx context.Context, r *sql.Rows) (T, error)

// SQLxResourceFunc is a function for scanning a single row from a sqlx.Rows iterator, and transforming it into a struct type.
type SQLxResourceFunc[T any] func(ctx context.Context, r *sqlx.Rows) (T, error)

// StaticListDataHandler is a handler for serving a list of resources from a static list.
// Data is retrieved from the given ListStaticDataFunc, and then serialized to the http.ResponseWriter.
// Errors encountered during the process are passed to the provided ErrorHandlerFunc.
func StaticListDataHandler[T any](dataFetcher ListStaticDataFunc[T], errorHandler ErrorHandlerFunc) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
logger := zerolog.Ctx(r.Context())
Expand Down Expand Up @@ -60,6 +73,11 @@ func StaticListDataHandler[T any](dataFetcher ListStaticDataFunc[T], errorHandle
})
}

// SQLListDataHandler is a handler for serving a list of resources from a SQL source.
// Data is retrieved via a sql.Rows iterator retrieved from the given ListSQLDataFunc,
// scanned into a struct via the SQLResourceFunc, and then serialized to the http.ResponseWriter.
// Serialization is buffered, so the entire result set is read before writing the response.
// Errors encountered during the process are passed to the provided ErrorHandlerFunc.
func SQLListDataHandler[T any](dataFetcher ListSQLDataFunc, dataTransformer SQLResourceFunc[T], errorHandler ErrorHandlerFunc) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
logger := zerolog.Ctx(r.Context())
Expand Down Expand Up @@ -134,6 +152,11 @@ func bufferSQLResults[T any](ctx context.Context, rows *sql.Rows, dataTransforme
return results, nil
}

// SQLxListDataHandler is a handler for serving a list of resources from a SQL source via sqlx.
// Data is retrieved via a sqlx.Rows iterator retrieved from the given ListSQLxDataFunc,
// scanned into a struct via the SQLxResourceFunc, and then serialized to the http.ResponseWriter.
// Serialization is buffered, so the entire result set is read before writing the response.
// Errors encountered during the process are passed to the provided ErrorHandlerFunc.
func SQLxListDataHandler[T any](dataFetcher ListSQLxDataFunc, dataTransformer SQLxResourceFunc[T], errorHandler ErrorHandlerFunc) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
logger := zerolog.Ctx(r.Context())
Expand Down Expand Up @@ -208,6 +231,11 @@ func bufferSQLxResults[T any](ctx context.Context, rows *sqlx.Rows, dataTransfor
return results, nil
}

// ResourceDataHandler is a handler for serving a single resource. Data is retrieved from the
// given ResourceDataFunc, and then serialized to the http.ResponseWriter.
// If the response is an io.Reader, the response is streamed to the client via StreamResponse.
// Otherwise, the entire result set is read before writing the response.
// Errors encountered during the process are passed to the provided ErrorHandlerFunc.
func ResourceDataHandler[T any](dataFetcher ResourceDataFunc[T], errorHandler ErrorHandlerFunc) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
logger := zerolog.Ctx(r.Context())
Expand Down Expand Up @@ -253,6 +281,10 @@ func ResourceDataHandler[T any](dataFetcher ResourceDataFunc[T], errorHandler Er
})
}

// StreamResponse streams the provided io.Reader to the http.ResponseWriter. The function
// tries to determine the content type of the stream by reading the first 512 bytes, and sets
// the content-type HTTP header accordingly.
// Errors encountered during the process are passed to the provided ErrorHandlerFunc.
func StreamResponse(reader io.Reader, w http.ResponseWriter, r *http.Request, errorHandler ErrorHandlerFunc) {
logger := zerolog.Ctx(r.Context())

Expand Down
8 changes: 8 additions & 0 deletions middleware_file.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import (
"net/http"
)

// FileHandleFunc is a function that handles a single file upload.
type FileHandleFunc func(ctx context.Context, entityUUID, userUUID string, fileName string, file multipart.File) error

// IsHandledByDefaultFileUploadErrorHandler indicates if the DefaultFileUploadErrorHandler has any special
Expand All @@ -20,6 +21,7 @@ func IsHandledByDefaultFileUploadErrorHandler(err error) bool {
IsHandledByDefaultErrorHandler(err)
}

// DefaultFileUploadErrorHandler is a default error handler, which sensibly handles errors known by turtleware.
func DefaultFileUploadErrorHandler(ctx context.Context, w http.ResponseWriter, r *http.Request, err error) {
if errors.Is(err, http.ErrNotMultipart) ||
errors.Is(err, http.ErrMissingBoundary) ||
Expand All @@ -31,6 +33,9 @@ func DefaultFileUploadErrorHandler(ctx context.Context, w http.ResponseWriter, r
DefaultErrorHandler(ctx, w, r, err)
}

// FileUploadMiddleware is a middleware that handles uploads of one or multiple files.
// Uploads are parsed from the request via HandleFileUpload, and then passed to the provided FileHandleFunc.
// Errors encountered during the process are passed to the provided ErrorHandlerFunc.
func FileUploadMiddleware(fileHandleFunc FileHandleFunc, errorHandler ErrorHandlerFunc) func(http.Handler) http.Handler {
return func(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
Expand All @@ -50,6 +55,9 @@ func FileUploadMiddleware(fileHandleFunc FileHandleFunc, errorHandler ErrorHandl
}
}

// HandleFileUpload is a helper function for handling file uploads.
// It parses upload metadata from the request, and then calls the provided FileHandleFunc for each file part.
// Errors encountered during the process are passed to the caller.
func HandleFileUpload(ctx context.Context, r *http.Request, fileHandleFunc FileHandleFunc) error {
logger := zerolog.Ctx(ctx)

Expand Down
19 changes: 16 additions & 3 deletions middleware_patch.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,14 +11,23 @@ import (
)

var (
// ErrUnmodifiedSinceHeaderMissing is returned when the If-Unmodified-Since header is missing.
ErrUnmodifiedSinceHeaderMissing = errors.New("If-Unmodified-Since header missing")

// ErrUnmodifiedSinceHeaderInvalid is returned when the If-Unmodified-Since header is in an invalid format.
ErrUnmodifiedSinceHeaderInvalid = errors.New("received If-Unmodified-Since header in invalid format")
ErrNoChanges = errors.New("patch request did not contain any changes")
ErrNoDateTimeLayoutMatched = errors.New("no date time layout matched")

// ErrNoChanges is returned when the patch request did not contain any changes.
ErrNoChanges = errors.New("patch request did not contain any changes")

// ErrNoDateTimeLayoutMatched is returned when the If-Unmodified-Since header does not match any known date time layout.
ErrNoDateTimeLayoutMatched = errors.New("no date time layout matched")
)

// PatchFunc is a function called for delegating the actual updating of an existing resource.
type PatchFunc[T PatchDTO] func(ctx context.Context, entityUUID, userUUID string, patch T, ifUnmodifiedSince time.Time) error

// PatchDTO defines the contract for validating a DTO used for patching a new resource.
type PatchDTO interface {
HasChanges() bool
Validate() []error
Expand All @@ -33,6 +42,7 @@ func IsHandledByDefaultPatchErrorHandler(err error) bool {
IsHandledByDefaultErrorHandler(err)
}

// DefaultPatchErrorHandler is a default error handler, which sensibly handles errors known by turtleware.
func DefaultPatchErrorHandler(ctx context.Context, w http.ResponseWriter, r *http.Request, err error) {
if errors.Is(err, ErrUnmodifiedSinceHeaderInvalid) || errors.Is(err, ErrNoChanges) {
WriteError(ctx, w, r, http.StatusBadRequest, err)
Expand All @@ -47,6 +57,9 @@ func DefaultPatchErrorHandler(ctx context.Context, w http.ResponseWriter, r *htt
DefaultErrorHandler(ctx, w, r, err)
}

// ResourcePatchMiddleware is a middleware for patching or updating an existing resource.
// It parses a PatchDTO from the request body, validates it, and then calls the provided PatchFunc.
// Errors encountered during the process are passed to the provided ErrorHandlerFunc.
func ResourcePatchMiddleware[T PatchDTO](patchFunc PatchFunc[T], errorHandler ErrorHandlerFunc) func(http.Handler) http.Handler {
return func(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
Expand Down Expand Up @@ -104,7 +117,7 @@ func ResourcePatchMiddleware[T PatchDTO](patchFunc PatchFunc[T], errorHandler Er
}
}

// GetIfUnmodifiedSince tries to parse the last modification (If-Modified-Since) header from
// GetIfUnmodifiedSince tries to parse a time.Time from the If-Unmodified-Since header of
// a given request. It tries the following formats (in that order):
//
// - time.RFC1123
Expand Down
6 changes: 3 additions & 3 deletions paging.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,15 +15,15 @@ type Paging struct {

var (
// ErrInvalidOffset indicates that the query contained an invalid
// offset parameter (e.g. non numeric).
// offset parameter (e.g. non-numeric).
ErrInvalidOffset = errors.New("invalid offset parameter")

// ErrInvalidLimit indicates that the query contained an invalid
// limit parameter (e.g. non numeric).
// limit parameter (e.g. non-numeric).
ErrInvalidLimit = errors.New("invalid limit parameter")
)

// ParsePagingFromRequest parses paging information from a given
// ParsePagingFromRequest parses Paging information from a given
// request.
func ParsePagingFromRequest(r *http.Request) (Paging, error) {
query := r.URL.Query()
Expand Down
Loading

0 comments on commit 317846d

Please sign in to comment.