handlerwrap

package module
v0.2.1 Latest Latest
Warning

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

Go to latest
Published: Nov 21, 2023 License: MIT Imports: 11 Imported by: 0

README

handlerwrap

import "github.com/induzo/gocom/http/handlerwrap"

This package allows you to wrap your API to save you from boilerplate code

Index

Constants

Query parameter keys used for cursor-based pagination.

const (
    StartingAfterKey = "starting_after"
    EndingBeforeKey  = "ending_before"
    LimitKey         = "limit"
)

ForwardPagination and BackwardPagination indicate the direction of pagination.

const (
    ForwardPagination  = "forward"
    BackwardPagination = "backward"
)
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")
)

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.

type Encoding

Encoding is the media type used to render the returned content.

type Encoding string
const (
    ApplicationJSON Encoding = "application/json"
)
func ParseAcceptedEncoding
func ParseAcceptedEncoding(req *http.Request) Encoding

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 ErrResp

type ErrResp interface {
    Render(ctx context.Context, respW http.ResponseWriter, encoding Encoding)
    Log(log *slog.Logger)
    IsNil() bool
}

type ErrorResponse

ErrorResponse is a wrapper for the error response body to have a clean way of displaying errors.

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"`
}
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

InternalServerError is an error that is returned when an internal server error occurs.

type InternalServerError struct {
    Err error
}
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 L10NError

L10NError is an error for localization

type L10NError struct {
    TitleKey   string `json:"title_key"`
    MessageKey string `json:"message_key"`
}

type MediaType

MediaType holds the type, subtype and parameters of a media type.

type MediaType struct {
    Type       string
    Subtype    string
    Parameters Parameters
}
func GetMediaType
func GetMediaType(request *http.Request) (MediaType, error)

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
func NewMediaType(s string) MediaType

NewMediaType parses the string and returns an instance of MediaType struct.

func ParseMediaType
func ParseMediaType(inputStr string) (MediaType, error)

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
func (mediaType MediaType) Equal(mt MediaType) bool

Equal checks whether the provided MIME media type matches this one including all parameters

func (MediaType) EqualsMIME
func (mediaType MediaType) EqualsMIME(mt MediaType) bool

EqualsMIME checks whether the base MIME types match

func (MediaType) IsWildcard
func (mediaType MediaType) IsWildcard() bool

IsWildcard returns true if either the Type or Subtype are the wildcard character '*'

func (MediaType) MIME
func (mediaType MediaType) MIME() string

MIME returns the MIME type without any of the parameters

func (MediaType) Matches
func (mediaType MediaType) Matches(mt MediaType) bool

Matches checks whether the MIME media types match handling wildcards in either

func (MediaType) MatchesAny
func (mediaType MediaType) MatchesAny(mts ...MediaType) bool

MatchesAny checks whether the MIME media types matches any of the specified list of mediatype handling wildcards in any of them

func (*MediaType) String
func (mediaType *MediaType) String() string

Converts the MediaType to string.

type MissingParamError

MissingParamError is the error that is returned when a named URL param is missing.

type MissingParamError struct {
    Name string
}
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

NamedURLParamsGetter is the interface that is used to parse the URL parameters.

type NamedURLParamsGetter func(ctx context.Context, key string) (string, *ErrorResponse)

type NotFoundError

NotFoundError is an error that is returned when a resource is not found.

type NotFoundError struct {
    Designation string
}
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.

type PaginationParamError struct {
    StartingAfterValue string
    EndingBeforeValue  string
}
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

PaginationParams are the query parameters required for cursor-based pagination.

type PaginationParams struct {
    CursorValue     string
    CursorColumn    string
    CursorDirection string
    Limit           int
}
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 Parameters = map[string]string

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.

type ParseLimitError struct {
    Value    string
    MaxLimit int
}
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.

type ParsingParamError struct {
    Name  string
    Value string
}
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.

type Response struct {
    Headers    map[string]string
    Body       any
    StatusCode int
}
func (*Response) Render
func (hr *Response) Render(ctx context.Context, respW http.ResponseWriter, respEncoding Encoding)
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.

type TypedHandler[R Resp, ER ErrResp] func(r *http.Request) (R, ER)
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

Generated by gomarkdoc

Documentation

Overview

This package allows you to wrap your API to save you from boilerplate code

Index

Examples

Constants

View Source
const (
	StartingAfterKey = "starting_after"
	EndingBeforeKey  = "ending_before"
	LimitKey         = "limit"
)

Query parameter keys used for cursor-based pagination.

View Source
const (
	ForwardPagination  = "forward"
	BackwardPagination = "backward"
)

ForwardPagination and BackwardPagination indicate the direction of pagination.

View Source
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

View Source
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

func ParseAcceptedEncoding(req *http.Request) Encoding

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 ErrResp

type ErrResp interface {
	Render(ctx context.Context, respW http.ResponseWriter, encoding Encoding)
	Log(log *slog.Logger)
	IsNil() bool
}

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 L10NError

type L10NError struct {
	TitleKey   string `json:"title_key"`
	MessageKey string `json:"message_key"`
}

L10NError is an error for localization

type MediaType

type MediaType struct {
	Type       string
	Subtype    string
	Parameters Parameters
}

MediaType holds the type, subtype and parameters of a media type.

func GetMediaType

func GetMediaType(request *http.Request) (MediaType, error)

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

func NewMediaType(s string) MediaType

NewMediaType parses the string and returns an instance of MediaType struct.

func ParseMediaType

func ParseMediaType(inputStr string) (MediaType, error)

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

func (mediaType MediaType) Equal(mt MediaType) bool

Equal checks whether the provided MIME media type matches this one including all parameters

func (MediaType) EqualsMIME

func (mediaType MediaType) EqualsMIME(mt MediaType) bool

EqualsMIME checks whether the base MIME types match

func (MediaType) IsWildcard

func (mediaType MediaType) IsWildcard() bool

IsWildcard returns true if either the Type or Subtype are the wildcard character '*'

func (MediaType) MIME

func (mediaType MediaType) MIME() string

MIME returns the MIME type without any of the parameters

func (MediaType) Matches

func (mediaType MediaType) Matches(mt MediaType) bool

Matches checks whether the MIME media types match handling wildcards in either

func (MediaType) MatchesAny

func (mediaType MediaType) MatchesAny(mts ...MediaType) bool

MatchesAny checks whether the MIME media types matches any of the specified list of mediatype handling wildcards in any of them

func (*MediaType) String

func (mediaType *MediaType) String() string

Converts the MediaType to string.

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

type PaginationParamError struct {
	StartingAfterValue string
	EndingBeforeValue  string
}

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

type Parameters = map[string]string

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

type ParseLimitError struct {
	Value    string
	MaxLimit int
}

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

type ParsingParamError struct {
	Name  string
	Value string
}

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

type Response struct {
	Headers    map[string]string
	Body       any
	StatusCode int
}

Response is a wrapper for the response body.

func (*Response) Render

func (hr *Response) Render(ctx context.Context, respW http.ResponseWriter, respEncoding Encoding)
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

type TypedHandler[R Resp, ER ErrResp] func(r *http.Request) (R, ER)

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

Jump to

Keyboard shortcuts

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