Documentation
¶
Overview ¶
This package allows you to wrap your API to save you from boilerplate code
Index ¶
- Constants
- Variables
- func GetAcceptableMediaType(request *http.Request, availableMediaTypes []MediaType) (MediaType, Parameters, error)
- func GetAcceptableMediaTypeFromHeader(headerValue string, availableMediaTypes []MediaType) (MediaType, Parameters, error)
- func ParsePaginationQueryParams(urlValue *url.URL, paginationColumn string, defaultLimit, maxLimit int) (*PaginationParams, *ErrorResponse)
- func Render(ctx context.Context, headers map[string]string, statusCode int, ...)
- func Wrapper[R Resp, ER ErrResp](f TypedHandler[R, ER]) http.HandlerFunc
- type Encoding
- type ErrResp
- type ErrorResponse
- func (her *ErrorResponse) AddHeaders(headers map[string]string)
- func (her *ErrorResponse) IsCodeEqual(errR1 *ErrorResponse) bool
- func (her *ErrorResponse) IsEqual(errR1 *ErrorResponse) bool
- func (her *ErrorResponse) IsNil() bool
- func (her *ErrorResponse) Log(logger *slog.Logger)
- func (her *ErrorResponse) Render(ctx context.Context, respW http.ResponseWriter, respEncoding Encoding)
- type InternalServerError
- type L10NError
- type MediaType
- func (mediaType MediaType) Equal(mt MediaType) bool
- func (mediaType MediaType) EqualsMIME(mt MediaType) bool
- func (mediaType MediaType) IsWildcard() bool
- func (mediaType MediaType) MIME() string
- func (mediaType MediaType) Matches(mt MediaType) bool
- func (mediaType MediaType) MatchesAny(mts ...MediaType) bool
- func (mediaType *MediaType) String() string
- type MissingParamError
- type NamedURLParamsGetter
- type NotFoundError
- type PaginationParamError
- type PaginationParams
- type Parameters
- type ParseBodyError
- type ParseLimitError
- type ParsingParamError
- type Resp
- type Response
- type TypedHandler
Examples ¶
Constants ¶
const ( StartingAfterKey = "starting_after" EndingBeforeKey = "ending_before" LimitKey = "limit" )
Query parameter keys used for cursor-based pagination.
const ( ForwardPagination = "forward" BackwardPagination = "backward" )
ForwardPagination and BackwardPagination indicate the direction of pagination.
const ( // ErrCodeParsingBody is the error code returned to the user when there is an error parsing // the body of the request. ErrCodeParsingBody = "error_parsing_body" )
Variables ¶
var ( // ErrInvalidMediaType is returned when the media type in the Content-Type or Accept header is syntactically invalid. ErrInvalidMediaType = errors.New("invalid media type") // ErrInvalidMediaRange is returned when the range of media types in the Content-Type or // Accept header is syntactically invalid. ErrInvalidMediaRange = errors.New("invalid media range") // ErrInvalidParameter is returned when the media type parameter in the Content-Type or // Accept header is syntactically invalid. ErrInvalidParameter = errors.New("invalid parameter") // ErrInvalidExtensionParameter is returned when the media type extension parameter in the // Content-Type or Accept header is syntactically invalid. ErrInvalidExtensionParameter = errors.New("invalid extension parameter") // ErrNoAcceptableTypeFound is returned when Accept header contains only media types that // are not in the acceptable media type list. ErrNoAcceptableTypeFound = errors.New("no acceptable type found") // ErrNoAvailableTypeGiven is returned when the acceptable media type list is empty. ErrNoAvailableTypeGiven = errors.New("no available type given") // ErrInvalidWeight is returned when the media type weight in Accept header is syntactically invalid. ErrInvalidWeight = errors.New("invalid weight") )
Functions ¶
func GetAcceptableMediaType ¶
func GetAcceptableMediaType(request *http.Request, availableMediaTypes []MediaType) (MediaType, Parameters, error)
GetAcceptableMediaType chooses a media type from available media types according to the Accept. Returns the most suitable media type or an error if no type can be selected.
func GetAcceptableMediaTypeFromHeader ¶
func GetAcceptableMediaTypeFromHeader( headerValue string, availableMediaTypes []MediaType, ) (MediaType, Parameters, error)
GetAcceptableMediaTypeFromHeader chooses a media type from available media types according to the specified Accept header value. Returns the most suitable media type or an error if no type can be selected.
func ParsePaginationQueryParams ¶
func ParsePaginationQueryParams( urlValue *url.URL, paginationColumn string, defaultLimit, maxLimit int, ) (*PaginationParams, *ErrorResponse)
ParsePaginationQueryParams parses query parameters: starting_after, ending_before and limit from a URL and returns the corresponding PaginationParams.
starting_after and ending_before are object IDs that define the place in the list and are optional. starting_after is used to fetch the next page of the list (forward pagination) while ending_before is used to fetch the previous page of the list (backward pagination). Returns error if both keys are used together. If no value is provided, PaginationParams.CursorValue will be set to the empty string.
limit is the number of objects to be returned and is optional. Returns error if limit is not a valid integer between 1 and maxLimit. If no value is provided, PaginationParams.Limit will be set to defaultLimit.
Example ¶
Get query parameters using ParsePaginationQueryParams in handler
package main import ( "context" "fmt" "io" "log/slog" "net/http" "net/http/httptest" "github.com/induzo/gocom/contextslogger" "github.com/induzo/gocom/http/handlerwrap" ) func main() { listHandler := func() handlerwrap.TypedHandler[*handlerwrap.Response, *handlerwrap.ErrorResponse] { return func(r *http.Request) (*handlerwrap.Response, *handlerwrap.ErrorResponse) { paginationParams, err := handlerwrap.ParsePaginationQueryParams(r.URL, "id", 10, 100) if err != nil { return nil, err } return &handlerwrap.Response{ Body: paginationParams.Limit, Headers: make(map[string]string), StatusCode: http.StatusOK, }, nil } } req, _ := http.NewRequestWithContext(context.Background(), http.MethodGet, "/limit?=10", nil) req = req.WithContext(contextslogger.NewContext(req.Context(), slog.New(slog.NewTextHandler(io.Discard, nil)))) nr := httptest.NewRecorder() handlerwrap.Wrapper(listHandler()).ServeHTTP(nr, req) rr := nr.Result() defer rr.Body.Close() body, _ := io.ReadAll(rr.Body) fmt.Println(string(body)) }
Output: 10
func Render ¶
func Render( ctx context.Context, headers map[string]string, statusCode int, responseBody interface{}, respEncoding Encoding, respW http.ResponseWriter, )
Render renders a http response, where the content type the response should take is specified by encoding. "application/json" is the default content type.
Example ¶
Render.
package main import ( "context" "fmt" "io" "log/slog" "net/http" "net/http/httptest" "github.com/induzo/gocom/contextslogger" "github.com/induzo/gocom/http/handlerwrap" ) func main() { handler := func() http.HandlerFunc { return http.HandlerFunc(func(respW http.ResponseWriter, req *http.Request) { body := struct { Test int `json:"test"` }{Test: 123} headers := map[string]string{} statusCode := http.StatusOK handlerwrap.Render(req.Context(), headers, statusCode, body, handlerwrap.ApplicationJSON, respW) }) } mux := http.NewServeMux() mux.Handle("/", handler()) req, _ := http.NewRequestWithContext(context.Background(), http.MethodGet, "/", nil) req = req.WithContext(contextslogger.NewContext(req.Context(), slog.New(slog.NewTextHandler(io.Discard, nil)))) nr := httptest.NewRecorder() mux.ServeHTTP(nr, req) rr := nr.Result() defer rr.Body.Close() body, _ := io.ReadAll(rr.Body) fmt.Println(string(body)) }
Output: {"test":123}
func Wrapper ¶
func Wrapper[R Resp, ER ErrResp](f TypedHandler[R, ER]) http.HandlerFunc
Wrapper will actually do the boring work of logging an error and render the response.
Types ¶
type Encoding ¶
type Encoding string
Encoding is the media type used to render the returned content.
const (
ApplicationJSON Encoding = "application/json"
)
func ParseAcceptedEncoding ¶
ParseAcceptedEncoding is used to parse the Accept header from the request and match it to supported types to render the response with. The default content type if there are no matches is "application/json".
Example ¶
Use ParseAcceptedEncoding to get the encoding and use it to render the http response.
package main import ( "context" "fmt" "io" "log/slog" "net/http" "net/http/httptest" "github.com/induzo/gocom/contextslogger" "github.com/induzo/gocom/http/handlerwrap" ) func main() { handler := func() http.HandlerFunc { return http.HandlerFunc(func(respW http.ResponseWriter, req *http.Request) { body := struct { Test int `json:"test"` }{Test: 123} headers := map[string]string{} statusCode := http.StatusOK encoding := handlerwrap.ParseAcceptedEncoding(req) handlerwrap.Render(req.Context(), headers, statusCode, body, encoding, respW) }) } mux := http.NewServeMux() mux.Handle("/", handler()) req, _ := http.NewRequestWithContext(context.Background(), http.MethodGet, "/", nil) req = req.WithContext(contextslogger.NewContext(req.Context(), slog.New(slog.NewTextHandler(io.Discard, nil)))) nr := httptest.NewRecorder() mux.ServeHTTP(nr, req) rr := nr.Result() defer rr.Body.Close() body, _ := io.ReadAll(rr.Body) fmt.Println(string(body)) }
Output: {"test":123}
type ErrorResponse ¶
type ErrorResponse struct { Err error `json:"-"` Headers map[string]string `json:"-"` StatusCode int `json:"-"` Error string `json:"error"` ErrorMessage string `json:"error_message"` L10NError *L10NError `json:"l10n_error,omitempty"` AdditionalInfo interface{} `json:"additional_info,omitempty"` }
ErrorResponse is a wrapper for the error response body to have a clean way of displaying errors.
func BindBody ¶
func BindBody(r *http.Request, target interface{}) *ErrorResponse
BindBody will bind the body of the request to the given interface.
func NewErrorResponse ¶
func NewErrorResponse( err error, headers map[string]string, statusCode int, errCode string, msg string, ) *ErrorResponse
NewErrorResponse creates a new ErrorResponse.
func NewUserErrorResponse ¶
func NewUserErrorResponse( err error, headers map[string]string, statusCode int, errCode string, msg string, titleKey string, msgKey string, ) *ErrorResponse
NewUserErrorResponse create a new ErrorResponse with L10NError
func (*ErrorResponse) AddHeaders ¶
func (her *ErrorResponse) AddHeaders(headers map[string]string)
AddHeaders add the headers to the error response it will overwrite a header if it already present, but will leave others in place
func (*ErrorResponse) IsCodeEqual ¶
func (her *ErrorResponse) IsCodeEqual(errR1 *ErrorResponse) bool
IsCodeEqual compare the error code, status code and L10Error, etc. The fields might be used for client. Test the error message and error can easily lead to fragile test case. You can leverage this function in you testing to compare between the expectation and actual error response.
func (*ErrorResponse) IsEqual ¶
func (her *ErrorResponse) IsEqual(errR1 *ErrorResponse) bool
IsEqual checks if an error response is equal to another. If using custom error structs in Err field, they should implement Is method for this to work.
func (*ErrorResponse) IsNil ¶
func (her *ErrorResponse) IsNil() bool
IsNil will determine if it is empty or not
func (*ErrorResponse) Log ¶
func (her *ErrorResponse) Log( logger *slog.Logger, )
func (*ErrorResponse) Render ¶
func (her *ErrorResponse) Render( ctx context.Context, respW http.ResponseWriter, respEncoding Encoding, )
Example ¶
Render error response.
package main import ( "context" "fmt" "io" "log/slog" "net/http" "net/http/httptest" "github.com/induzo/gocom/contextslogger" "github.com/induzo/gocom/http/handlerwrap" ) func main() { handler := func() http.HandlerFunc { return http.HandlerFunc(func(respW http.ResponseWriter, req *http.Request) { errResp := handlerwrap.NewErrorResponse( fmt.Errorf("dummy err"), map[string]string{}, http.StatusInternalServerError, "dummy_err", "dummy err", ) errResp.Render(req.Context(), respW, handlerwrap.ApplicationJSON) }) } mux := http.NewServeMux() mux.Handle("/", handler()) req, _ := http.NewRequestWithContext(context.Background(), http.MethodGet, "/", nil) req = req.WithContext(contextslogger.NewContext(req.Context(), slog.New(slog.NewTextHandler(io.Discard, nil)))) nr := httptest.NewRecorder() mux.ServeHTTP(nr, req) rr := nr.Result() defer rr.Body.Close() body, _ := io.ReadAll(rr.Body) fmt.Println(string(body)) }
Output: {"error":"dummy_err","error_message":"dummy err"}
type InternalServerError ¶
type InternalServerError struct {
Err error
}
InternalServerError is an error that is returned when an internal server error occurs.
func (*InternalServerError) Error ¶
func (e *InternalServerError) Error() string
func (*InternalServerError) Is ¶
func (e *InternalServerError) Is(err error) bool
func (*InternalServerError) ToErrorResponse ¶
func (e *InternalServerError) ToErrorResponse() *ErrorResponse
func (*InternalServerError) Unwrap ¶
func (e *InternalServerError) Unwrap() error
type MediaType ¶
type MediaType struct { Type string Subtype string Parameters Parameters }
MediaType holds the type, subtype and parameters of a media type.
func GetMediaType ¶
GetMediaType gets the content of Content-Type header, parses it, and returns the parsed MediaType. If the request does not contain the Content-Type header, an empty MediaType is returned.
func NewMediaType ¶
NewMediaType parses the string and returns an instance of MediaType struct.
func ParseMediaType ¶
ParseMediaType parses the given string as a MIME media type (with optional parameters) and returns it as a MediaType. If the string cannot be parsed an appropriate error is returned.
func (MediaType) Equal ¶
Equal checks whether the provided MIME media type matches this one including all parameters
func (MediaType) EqualsMIME ¶
EqualsMIME checks whether the base MIME types match
func (MediaType) IsWildcard ¶
IsWildcard returns true if either the Type or Subtype are the wildcard character '*'
func (MediaType) Matches ¶
Matches checks whether the MIME media types match handling wildcards in either
func (MediaType) MatchesAny ¶
MatchesAny checks whether the MIME media types matches any of the specified list of mediatype handling wildcards in any of them
type MissingParamError ¶
type MissingParamError struct {
Name string
}
MissingParamError is the error that is returned when a named URL param is missing.
func (*MissingParamError) Error ¶
func (e *MissingParamError) Error() string
func (*MissingParamError) Is ¶
func (e *MissingParamError) Is(err error) bool
func (*MissingParamError) ToErrorResponse ¶
func (e *MissingParamError) ToErrorResponse() *ErrorResponse
type NamedURLParamsGetter ¶
type NamedURLParamsGetter func(ctx context.Context, key string) (string, *ErrorResponse)
NamedURLParamsGetter is the interface that is used to parse the URL parameters.
type NotFoundError ¶
type NotFoundError struct {
Designation string
}
NotFoundError is an error that is returned when a resource is not found.
func (*NotFoundError) Error ¶
func (e *NotFoundError) Error() string
func (*NotFoundError) Is ¶
func (e *NotFoundError) Is(err error) bool
func (*NotFoundError) ToErrorResponse ¶
func (e *NotFoundError) ToErrorResponse() *ErrorResponse
type PaginationParamError ¶
PaginationParamError is the error that is returned when both starting_after and ending_before query parameters are provided.
func (*PaginationParamError) Error ¶
func (e *PaginationParamError) Error() string
func (*PaginationParamError) Is ¶
func (e *PaginationParamError) Is(err error) bool
func (*PaginationParamError) ToErrorResponse ¶
func (e *PaginationParamError) ToErrorResponse() *ErrorResponse
type PaginationParams ¶
type PaginationParams struct { CursorValue string CursorColumn string CursorDirection string Limit int }
PaginationParams are the query parameters required for cursor-based pagination.
func NewPaginationParams ¶
func NewPaginationParams(val, col, direction string, limit int) *PaginationParams
NewPaginationParams creates new PaginationParams.
type Parameters ¶
Parameters represents media type parameters as a key-value map.
type ParseBodyError ¶
type ParseBodyError struct {
Err error
}
func (*ParseBodyError) Error ¶
func (e *ParseBodyError) Error() string
func (*ParseBodyError) ToErrorResponse ¶
func (e *ParseBodyError) ToErrorResponse() *ErrorResponse
func (*ParseBodyError) Unwrap ¶
func (e *ParseBodyError) Unwrap() error
type ParseLimitError ¶
ParseLimitError is the error that is returned when the limit query parameter is invalid.
func (*ParseLimitError) Error ¶
func (e *ParseLimitError) Error() string
func (*ParseLimitError) Is ¶
func (e *ParseLimitError) Is(err error) bool
func (*ParseLimitError) ToErrorResponse ¶
func (e *ParseLimitError) ToErrorResponse() *ErrorResponse
type ParsingParamError ¶
ParsingParamError is the error that is returned when a named URL param is invalid.
func (*ParsingParamError) Error ¶
func (e *ParsingParamError) Error() string
func (*ParsingParamError) Is ¶
func (e *ParsingParamError) Is(err error) bool
func (*ParsingParamError) ToErrorResponse ¶
func (e *ParsingParamError) ToErrorResponse() *ErrorResponse
type Resp ¶
type Resp interface { // Render will render the response. Render(ctx context.Context, respW http.ResponseWriter, encoding Encoding) }
type Response ¶
Response is a wrapper for the response body.
func (*Response) Render ¶
Example ¶
Render response.
package main import ( "context" "fmt" "io" "log/slog" "net/http" "net/http/httptest" "github.com/induzo/gocom/contextslogger" "github.com/induzo/gocom/http/handlerwrap" ) func main() { handler := func() http.HandlerFunc { return http.HandlerFunc(func(respW http.ResponseWriter, req *http.Request) { resp := &handlerwrap.Response{ Body: map[string]any{ "hello": "world", }, Headers: make(map[string]string), StatusCode: http.StatusOK, } resp.Render(req.Context(), respW, handlerwrap.ApplicationJSON) }) } mux := http.NewServeMux() mux.Handle("/", handler()) req, _ := http.NewRequestWithContext(context.Background(), http.MethodGet, "/", nil) req = req.WithContext(contextslogger.NewContext(req.Context(), slog.New(slog.NewTextHandler(io.Discard, nil)))) nr := httptest.NewRecorder() mux.ServeHTTP(nr, req) rr := nr.Result() defer rr.Body.Close() body, _ := io.ReadAll(rr.Body) fmt.Println(string(body)) }
Output: {"hello":"world"}
type TypedHandler ¶
TypedHandler is the handler that you are actually handling the response.
Example (Get) ¶
Wrapping a GET http handler.
package main import ( "context" "fmt" "io" "log" "log/slog" "net/http" "net/http/httptest" "strconv" "github.com/induzo/gocom/contextslogger" "github.com/induzo/gocom/http/handlerwrap" ) func main() { getter := func(ctx context.Context, key string) (string, *handlerwrap.ErrorResponse) { if key == "id" { return "1", nil } missingParamErr := &handlerwrap.MissingParamError{Name: key} return "", missingParamErr.ToErrorResponse() } getHandler := func(nupg handlerwrap.NamedURLParamsGetter) handlerwrap.TypedHandler[*handlerwrap.Response, *handlerwrap.ErrorResponse] { return func(r *http.Request) (*handlerwrap.Response, *handlerwrap.ErrorResponse) { idParam, errR := nupg(r.Context(), "id") if errR != nil { return nil, errR } log.Println(idParam) id, err := strconv.ParseInt(idParam, 10, 64) if err != nil { parsingParamErr := &handlerwrap.ParsingParamError{ Name: "id", Value: idParam, } return nil, parsingParamErr.ToErrorResponse() } return &handlerwrap.Response{ Body: id, Headers: make(map[string]string), StatusCode: http.StatusOK, }, nil } } req, _ := http.NewRequestWithContext(context.Background(), http.MethodGet, "/", nil) req = req.WithContext(contextslogger.NewContext(req.Context(), slog.New(slog.NewTextHandler(io.Discard, nil)))) nr := httptest.NewRecorder() handlerwrap.Wrapper(getHandler(getter)).ServeHTTP(nr, req) rr := nr.Result() defer rr.Body.Close() fmt.Println(rr.StatusCode) }
Output: 200
Example (Post) ¶
Wrapping a POST http handler.
package main import ( "bytes" "context" "encoding/json" "fmt" "io" "log" "log/slog" "net/http" "net/http/httptest" "github.com/induzo/gocom/contextslogger" "github.com/induzo/gocom/http/handlerwrap" ) func main() { type postRequest struct { Name string `json:"name"` } createHandler := func() handlerwrap.TypedHandler[*handlerwrap.Response, *handlerwrap.ErrorResponse] { return func(r *http.Request) (*handlerwrap.Response, *handlerwrap.ErrorResponse) { var pr postRequest if err := handlerwrap.BindBody(r, &pr); err != nil { return nil, err } log.Println(pr) return &handlerwrap.Response{ Body: pr, Headers: make(map[string]string), StatusCode: http.StatusCreated, }, nil } } reqBody, err := json.Marshal(postRequest{ Name: "test", }) if err != nil { log.Fatalf("marshal reqbody: %s", err) } req, _ := http.NewRequestWithContext(context.Background(), http.MethodGet, "/", bytes.NewBuffer(reqBody)) req = req.WithContext(contextslogger.NewContext(req.Context(), slog.New(slog.NewTextHandler(io.Discard, nil)))) nr := httptest.NewRecorder() handlerwrap.Wrapper(createHandler()).ServeHTTP(nr, req) rr := nr.Result() defer rr.Body.Close() fmt.Println(rr.StatusCode) }
Output: 201