diff --git a/composition.go b/composition.go index a0b0ecc..fc80da9 100644 --- a/composition.go +++ b/composition.go @@ -11,6 +11,7 @@ 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) @@ -18,6 +19,8 @@ type GetEndpoint[T any] interface { 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], @@ -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) @@ -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], @@ -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) @@ -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], @@ -88,6 +97,7 @@ 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) @@ -95,6 +105,8 @@ type GetStaticListEndpoint[T any] interface { 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], @@ -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], @@ -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], diff --git a/middleware_common.go b/middleware_common.go index 96d8a0f..c28e554 100644 --- a/middleware_common.go +++ b/middleware_common.go @@ -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) @@ -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) @@ -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 { diff --git a/middleware_create.go b/middleware_create.go index 1e26337..cfbf4cf 100644 --- a/middleware_create.go +++ b/middleware_create.go @@ -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 } @@ -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) { diff --git a/middleware_data.go b/middleware_data.go index 967cf3e..f6331e4 100644 --- a/middleware_data.go +++ b/middleware_data.go @@ -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()) @@ -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()) @@ -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()) @@ -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()) @@ -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()) diff --git a/middleware_file.go b/middleware_file.go index 96ef3a6..94d7e05 100644 --- a/middleware_file.go +++ b/middleware_file.go @@ -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 @@ -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) || @@ -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) { @@ -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) diff --git a/middleware_patch.go b/middleware_patch.go index e18dcab..1e65f8d 100644 --- a/middleware_patch.go +++ b/middleware_patch.go @@ -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 @@ -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) @@ -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) { @@ -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 diff --git a/paging.go b/paging.go index 72db24a..3464dd3 100755 --- a/paging.go +++ b/paging.go @@ -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() diff --git a/tenant/composition.go b/tenant/composition.go index e4fde2d..ae82b8d 100644 --- a/tenant/composition.go +++ b/tenant/composition.go @@ -12,6 +12,7 @@ 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, tenantUUID string, entityUUID string) (time.Time, error) @@ -19,6 +20,8 @@ type GetEndpoint[T any] interface { HandleError(ctx context.Context, w http.ResponseWriter, r *http.Request, err error) } +// ResourceHandler composes a full http.Handler for retrieving a single tenant scoped resource. +// This includes authentication, caching, and data retrieval. func ResourceHandler[T any]( keySet jwk.Set, getEndpoint GetEndpoint[T], @@ -37,6 +40,7 @@ func ResourceHandler[T any]( // -------------------------- +// GetSQLListEndpoint defines the contract for a ListSQLHandler composition. type GetSQLListEndpoint[T any] interface { ListHash(ctx context.Context, tenantUUID string, paging turtleware.Paging) (string, error) TotalCount(ctx context.Context, tenantUUID string) (uint, error) @@ -45,6 +49,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], @@ -63,6 +69,7 @@ func ListSQLHandler[T any]( // -------------------------- +// GetSQLxListEndpoint defines the contract for a ListSQLxHandler composition. type GetSQLxListEndpoint[T any] interface { ListHash(ctx context.Context, tenantUUID string, paging turtleware.Paging) (string, error) TotalCount(ctx context.Context, tenantUUID string) (uint, error) @@ -71,6 +78,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 SQLx. +// This includes authentication, caching, and data retrieval. func ListSQLxHandler[T any]( keySet jwk.Set, listEndpoint GetSQLxListEndpoint[T], @@ -89,6 +98,7 @@ func ListSQLxHandler[T any]( // -------------------------- +// GetStaticListEndpoint defines the contract for a StaticListHandler composition. type GetStaticListEndpoint[T any] interface { ListHash(ctx context.Context, tenantUUID string, paging turtleware.Paging) (string, error) TotalCount(ctx context.Context, tenantUUID string) (uint, error) @@ -96,6 +106,8 @@ type GetStaticListEndpoint[T any] interface { HandleError(ctx context.Context, w http.ResponseWriter, r *http.Request, err error) } +// StaticListHandler composes a full http.Handler for retrieving a list of resources via a static list. +// This includes authentication, caching, and data retrieval. func StaticListHandler[T any]( keySet jwk.Set, listEndpoint GetStaticListEndpoint[T], @@ -114,12 +126,15 @@ func StaticListHandler[T any]( // -------------------------- +// CreateEndpoint defines the contract for a ResourceCreateHandler composition. type CreateEndpoint[T turtleware.CreateDTO] interface { EntityUUID(r *http.Request) (string, error) CreateEntity(ctx context.Context, tenantUUID, 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 tenant scoped resource. +// This includes authentication, caching, and data retrieval. func ResourceCreateHandler[T turtleware.CreateDTO]( keySet jwk.Set, createEndpoint CreateEndpoint[T], @@ -138,12 +153,15 @@ func ResourceCreateHandler[T turtleware.CreateDTO]( // -------------------------- +// PatchEndpoint defines the contract for a ResourcePatchHandler composition. type PatchEndpoint[T turtleware.PatchDTO] interface { EntityUUID(r *http.Request) (string, error) UpdateEntity(ctx context.Context, tenantUUID, 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 a tenant scoped resource. +// This includes authentication, caching, and data retrieval. func ResourcePatchHandler[T turtleware.PatchDTO]( keySet jwk.Set, patchEndpoint PatchEndpoint[T], diff --git a/tenant/middleware_common.go b/tenant/middleware_common.go index 63aed71..71c68ab 100644 --- a/tenant/middleware_common.go +++ b/tenant/middleware_common.go @@ -21,8 +21,8 @@ var ( ErrContextMissingTenantUUID = errors.New("missing tenant UUID in context") // ErrTokenMissingTenantUUID indicates that a requested was - // missing the tenant uuid. - ErrTokenMissingTenantUUID = errors.New("token does not include tenant uuid") + // missing the tenant UUID. + ErrTokenMissingTenantUUID = errors.New("token does not include tenant UUID") ) // UUIDMiddleware is a http middleware for checking tenant authentication details, and @@ -48,6 +48,8 @@ func UUIDMiddleware(h http.Handler) http.Handler { }) } +// UUIDFromRequestContext extracts the tenant UUID from the request context. +// Returns ErrContextMissingTenantUUID if the tenant UUID is missing from the context. func UUIDFromRequestContext(ctx context.Context) (string, error) { tenantUUID, ok := ctx.Value(ctxTenantUUID).(string) if !ok { diff --git a/tenant/middleware_core.go b/tenant/middleware_core.go index 32625f4..e178edb 100644 --- a/tenant/middleware_core.go +++ b/tenant/middleware_core.go @@ -36,7 +36,7 @@ type ListCountFunc func(ctx context.Context, tenantUUID string) (uint, error) type ResourceLastModFunc func(ctx context.Context, tenantUUID string, entityUUID string) (time.Time, error) // CountHeaderMiddleware is a middleware for injecting an X-Total-Count header into the response, -// by the provided ListCountFunc. If an error is encountered, the provided ErrorHandlerFunc is called. +// by the provided ListCountFunc. If an error is encountered, the provided turtleware.ErrorHandlerFunc is called. func CountHeaderMiddleware( countFetcher ListCountFunc, errorHandler turtleware.ErrorHandlerFunc, @@ -78,7 +78,7 @@ func CountHeaderMiddleware( // if the If-None-Match header and the fetched hash differ. // If the ListHashFunc returns either sql.ErrNoRows or os.ErrNotExist, the sha256 hash of an // empty string is assumed as the hash. -// If an error is encountered, the provided ErrorHandlerFunc is called. +// If an error is encountered, the provided turtleware.ErrorHandlerFunc is called. func ListCacheMiddleware( hashFetcher ListHashFunc, errorHandler turtleware.ErrorHandlerFunc, @@ -144,7 +144,7 @@ func ListCacheMiddleware( // ResourceCacheMiddleware is a middleware for transparently handling caching of a single entity // (or resource) of a tenant via the provided ResourceLastModFunc. The next handler of the middleware // is only called when the If-Modified-Since header and the fetched last modification date differ. -// If an error is encountered, the provided ErrorHandlerFunc is called. +// If an error is encountered, the provided turtleware.ErrorHandlerFunc is called. func ResourceCacheMiddleware( lastModFetcher ResourceLastModFunc, errorHandler turtleware.ErrorHandlerFunc, diff --git a/tenant/middleware_create.go b/tenant/middleware_create.go index f257b14..649127b 100644 --- a/tenant/middleware_create.go +++ b/tenant/middleware_create.go @@ -9,8 +9,12 @@ import ( "net/http" ) +// CreateFunc is a function called for delegating the actual creating of a new tenant scoped resource. type CreateFunc[T turtleware.CreateDTO] func(ctx context.Context, tenantUUID, entityUUID, userUUID string, create T) error +// ResourceCreateMiddleware is a middleware for creating a new tenant scoped 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 turtleware.CreateDTO](createFunc CreateFunc[T], errorHandler turtleware.ErrorHandlerFunc) func(http.Handler) http.Handler { return func(next http.Handler) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { diff --git a/tenant/middleware_data.go b/tenant/middleware_data.go index cd8746d..e5bbcd4 100644 --- a/tenant/middleware_data.go +++ b/tenant/middleware_data.go @@ -13,11 +13,21 @@ import ( "os" ) +// ListStaticDataFunc is a function for retrieving a slice of data, scoped to the provided tenant and paging. type ListStaticDataFunc[T any] func(ctx context.Context, tenantUUID string, paging turtleware.Paging) ([]T, error) + +// ListSQLDataFunc is a function for retrieving a sql.Rows iterator, scoped to the provided tenant and paging. type ListSQLDataFunc func(ctx context.Context, tenantUUID string, paging turtleware.Paging) (*sql.Rows, error) + +// ListSQLxDataFunc is a function for retrieving a sqlx.Rows iterator, scoped to the provided tenant and paging. type ListSQLxDataFunc func(ctx context.Context, tenantUUID string, paging turtleware.Paging) (*sqlx.Rows, error) + +// ResourceDataFunc is a function for retrieving a single tenant scoped resource via its UUID. type ResourceDataFunc[T any] func(ctx context.Context, tenantUUID string, entityUUID string) (T, error) +// StaticListDataHandler is a handler for serving a list of tenant scoped 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 turtleware.ErrorHandlerFunc. func StaticListDataHandler[T any](dataFetcher ListStaticDataFunc[T], errorHandler turtleware.ErrorHandlerFunc) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { logger := zerolog.Ctx(r.Context()) @@ -60,6 +70,11 @@ func StaticListDataHandler[T any](dataFetcher ListStaticDataFunc[T], errorHandle }) } +// SQLListDataHandler is a handler for serving a list of tenant scoped resources from a SQL source. +// Data is retrieved via a sql.Rows iterator retrieved from the given ListSQLDataFunc, +// scanned into a struct via the turtleware.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 turtleware.ErrorHandlerFunc. func SQLListDataHandler[T any](dataFetcher ListSQLDataFunc, dataTransformer turtleware.SQLResourceFunc[T], errorHandler turtleware.ErrorHandlerFunc) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { logger := zerolog.Ctx(r.Context()) @@ -135,6 +150,11 @@ func bufferSQLResults[T any](ctx context.Context, rows *sql.Rows, dataTransforme return results, nil } +// SQLxListDataHandler is a handler for serving a list of tenant scoped 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 turtleware.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 turtleware.ErrorHandlerFunc. func SQLxListDataHandler[T any](dataFetcher ListSQLxDataFunc, dataTransformer turtleware.SQLxResourceFunc[T], errorHandler turtleware.ErrorHandlerFunc) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { logger := zerolog.Ctx(r.Context()) @@ -210,6 +230,11 @@ func bufferSQLxResults[T any](ctx context.Context, rows *sqlx.Rows, dataTransfor return results, nil } +// ResourceDataHandler is a handler for serving a single tenant scoped 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 turtleware.StreamResponse. +// Otherwise, the entire result set is read before writing the response. +// Errors encountered during the process are passed to the provided turtleware.ErrorHandlerFunc. func ResourceDataHandler[T any](dataFetcher ResourceDataFunc[T], errorHandler turtleware.ErrorHandlerFunc) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { logger := zerolog.Ctx(r.Context()) diff --git a/tenant/middleware_file.go b/tenant/middleware_file.go index 628b3fd..9fedea1 100644 --- a/tenant/middleware_file.go +++ b/tenant/middleware_file.go @@ -8,8 +8,12 @@ import ( "net/http" ) +// FileHandleFunc is a function that handles a single tenant scoped file upload. type FileHandleFunc func(ctx context.Context, tenantUUID, entityUUID, userUUID string, fileName string, file multipart.File) error +// FileUploadMiddleware is a middleware that handles uploads of one or multiple files. +// Uploads are parsed from the request via turtleware.HandleFileUpload, and then passed to the provided FileHandleFunc. +// Errors encountered during the process are passed to the provided turtleware.ErrorHandlerFunc. func FileUploadMiddleware(partHandlerFunc FileHandleFunc, errorHandler turtleware.ErrorHandlerFunc) func(http.Handler) http.Handler { return func(next http.Handler) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { diff --git a/tenant/middleware_patch.go b/tenant/middleware_patch.go index fb8b285..fca8780 100644 --- a/tenant/middleware_patch.go +++ b/tenant/middleware_patch.go @@ -10,8 +10,12 @@ import ( "time" ) +// PatchFunc is a function called for delegating the actual updating of an existing tenant scoped resource. type PatchFunc[T turtleware.PatchDTO] func(ctx context.Context, tenantUUID, entityUUID, userUUID string, patch T, ifUnmodifiedSince time.Time) error +// ResourcePatchMiddleware is a middleware for patching or updating an existing tenant scoped resource. +// It parses a turtleware.PatchDTO from the request body, validates it, and then calls the provided PatchFunc. +// Errors encountered during the process are passed to the provided turtleware.ErrorHandlerFunc. func ResourcePatchMiddleware[T turtleware.PatchDTO](patchFunc PatchFunc[T], errorHandler turtleware.ErrorHandlerFunc) func(http.Handler) http.Handler { return func(next http.Handler) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { diff --git a/testdata/errorhandler/common/error_errmissinguseruuid.json b/testdata/errorhandler/common/error_errmissinguseruuid.json index fabdb5e..306d926 100644 --- a/testdata/errorhandler/common/error_errmissinguseruuid.json +++ b/testdata/errorhandler/common/error_errmissinguseruuid.json @@ -2,6 +2,6 @@ "status": 400, "text": "Bad Request", "errors": [ - "token does not include user uuid" + "token does not include user UUID" ] } \ No newline at end of file