typed

package
v1.16.3 Latest Latest
Warning

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

Go to latest
Published: Jul 1, 2024 License: Apache-2.0 Imports: 15 Imported by: 0

README

Chioas Typed Handlers

GoDoc Latest Version Go Report Card

Having http.HandlerFunc like...

func postPet(w http.ResponseWriter, r *http.Request)

Is, in many ways, very convenient... it's a common de-facto practice, it's flexible (can unmarshal requests and marshal responses directly)
However, there are some drawbacks...

  • you have to read the code to understand what the request and response should be
  • not great for APIs that support multiple content types (e.g. the ability to POST in either application/json or text/xml)
  • errors have to translated into http status codes (and messages unmarshalled into response body)

So Chioas Typed Handlers solves this by allowing, for example...

func postPet(req AddPetRequest) (Pet, error)

How To Use

To optionally use Chioas Typed Handlers just replace the default handler builder...

chioas.Definition{
    MethodHandlerBuilder: typed.NewTypedMethodsHandlerBuilder()
}

Handler Input Args

Chioas Typed Handlers looks at the types of each handler arg to determine what needs to be passed. This is based on the following rules...

Signature Description
func eg(w http.ResponseWriter) w will be the original http.ResponseWriter
func eg(r *http.Request) r will be the original *http.Request
func eg(ctx context.Context) ctx will be the context from original *http.Request
func eg(ctx *chi.Context) ctx will be the Chi context extracted from original *http.Request
func eg(ctx chi.Context) ctx will be the Chi context extracted from original *http.Request
func eg(hdrs http.Header) hdrs will be the request headers from original *http.Request
func eg(hdrs typed.Headers) hdrs will be the request headers from original *http.Request
func eg(hdrs map[string][]string) hdrs will be the request headers from original *http.Request
func eg(pps typed.PathParams) pps will be the path params extracted from *http.Request
func eg(cookies []*http.Cookie) cookies will be the cookies extracted from *http.Request
func eg(url *url.URL) url will be the URL from original *http.Request
func eg(pps ...string) pps will be the path param values
func eg(pp1 string, pp2 string) pp1 will be the first path param value, pp2 will be the second path param value etc.
func eg(pp1 string, pps ...string) pp1 will be the first path param value, pps will be the remaining path param values
func eg(qps typed.QueryParams) qps will be the request query params from *http.Request
func eg(qps typed.RawQuery) qps will be the raw request query params string from *http.Request
func eg(frm typed.PostForm) frm will be the post form extracted from the *http.Request
func eg(auth typed.BasicAuth) auth will be the basic auth extracted from *http.Request
func eg(auth *typed.BasicAuth) auth will be the basic auth extracted from *http.Request or nil if no Authorization header present
func eg(req []byte) req will be the request body read from *http.Request (see also note 2 below)
func eg(req MyStruct) req will be the request body read from *http.Request and unmarshalled into a MyStruct (see also note 2 below)
func eg(req *MyStruct) req will be the request body read from *http.Request and unmarshalled into a *MyStruct (see also note 2 below)
func eg(req []MyStruct) req will be the request body read from *http.Request and unmarshalled into a slice of []MyStruct (see also note 2 below)
func eg(b bool) will cause an error when setting up routes (see note 4 below)
func eg(i int) will cause an error when setting up routes (see note 4 below)
etc. will cause an error when setting up routes (see note 4 below)
Notes
  1. Multiple input args can be specified - the same rules apply
  2. If there are multiple arg types that involve reading the request body - this is reported as an error when setting up routes
  3. To support for other arg types - provide an implementation of typedArgBuilder passed as an option to typed.NewTypedMethodsHandlerBuilder(options ...any)
  4. Any other arg types will cause an error when setting up routes (unless supported by note 3)

Handler Return Args

Having called the handler, Chioas Typed Handlers looks at the return arg types to determine what needs to be written to the http.ResponseWriter. (Note: if there are no return args - then nothing is written to http.ResponseWriter)

There can be up to 3 return args for typed handlers:

  1. an error - indicating an error response needs to be written
  2. an int - indicating the response status code
  3. anything that is marshallable

Notes:

  • the order of the return arg types is not significant
  • any typed handler may not return more than one error arg, more than one int arg or more than one marshallable arg (failing to abide by this will cause an error when setting up routes)

Marshallable return args can be:

Return arg type Description
typed.ResponseMarshaler the ResponseMarshaler.Marshal method is called to determine what body, headers and status code should be written
typed.JsonResponse the fields of typed.JsonResponse are used to determine what body, headers and status code should be written (unless JsonResponse.Error is set)
*typed.JsonResponse same as typed.JsonResponse - unless nil, in which case no body is written and status code is set to 204 No Content
[]byte the raw byte data is written to the body and status code is set to 200 OK (or 204 No Content if slice is empty)
anything else the value is marshalled to JSON, Content-Type header is set to application/json and status code is set to 200 OK (unless an error occurs marshalling)
any / interface{} the actual type is assessed and dealt with according to the above rules. The actual type could also be error
Error Handling

By default, any error is handled by setting the response status to 500 Internal Server Error and nothing is written to the response body - unless...

If the error implements typed.ApiError - in which case the status code is set from ApiError.StatusCode()

If the error implements json.Marshaler - then the response body is the JSON marshalled error.

All of this can be overridden by providing a typed.ErrorHandler as an option to typed.NewTypedMethodsHandlerBuilder(options ...any) (or if your api instance implements the typed.ErrorHandler interface)

Return arg examples

The following table lists various combinations of return arg types with explanation of behaviour:

Example & explanation
func(...) error
  • if the error is non-nil then error information is written to http.ResponseWriter
  • otherwise nothing is written
func(...) (MyStruct, error)
  • if the error is non-nil then error information is written to http.ResponseWriter
  • otherwise the MyStruct is marshalled as JSON and written to http.ResponseWriter
func(...) (*MyStruct, error)
  • if the error is non-nil then error information is written to http.ResponseWriter
  • if the *MyStruct is non-nil then it is marshalled as JSON and written to http.ResponseWriter
  • otherwise nothing is written
func(...) (*MyStruct, int, error)
  • if the error is non-nil then error information is written to http.ResponseWriter
  • if the *MyStruct is non-nil then it is marshalled as JSON and written to http.ResponseWriter (and using the int as the default status code)
  • otherwise the int is written as the status code (nothing is written)
func(...) (any, error)
  • if the error is non-nil then error information is written to http.ResponseWriter
  • if the any arg is non-nil then it is marshalled as JSON and written to http.ResponseWriter
  • otherwise nothing is written
func(...) any
  • if the any arg is an error (non-nil) then error information is written to http.ResponseWriter
  • if the any arg is non-nil then it is marshalled as JSON and written to http.ResponseWriter
  • otherwise nothing is written

How Does It Work

Yes, Chioas Typed Handlers uses reflect to make the call to your typed handler (unless the handler is already a http.HandlerFunc)

As with anything that uses reflect, there is a performance price to pay. Although Chioas Typed Handlers attempts to minimize this by gathering all the type information for the type handler up-front - so handler arg types are only interrogated once.

But if you're concerned with ultimate performance or want to stick to convention - Chioas Typed Handlers is optional and entirely your own prerogative to use it. Or if only some endpoints in your API are performance sensitive but other endpoints would benefit from readability (or flexible content handling; or improved error handling) then you can mix-and-match.

Comparative Benchmarks

The following is a table of comparative benchmarks - between traditional handlers (i.e.http.HandlerFunc) and typed handlers...

  • GET is based on reading a single path param and writing a marshalled struct response
  • PUT is based on reading a single path param and unmarshalling a request struct - then writing a marshalled struct response
Operation ns/op B/op allocs/op sloc cyclo
GET traditional 1851 1857 16 15 3
GET typed 2636 2009 21 6 2
PUT traditional 3371 2897 28 21 4
PUT typed 4165 3074 33 6 2

Working example

The following is a short working example of typed handlers...

package main

import (
    "github.com/go-andiamo/chioas"
    "github.com/go-andiamo/chioas/typed"
    "github.com/go-chi/chi/v5"
    "net/http"
)

var def = chioas.Definition{
    DocOptions: chioas.DocOptions{
        ServeDocs: true,
    },
    MethodHandlerBuilder: typed.NewTypedMethodsHandlerBuilder(),
    Methods: map[string]chioas.Method{
        http.MethodGet: {
            Handler: func() (map[string]any, error) {
                return map[string]any{
                    "root": "root discovery",
                }, nil
            },
        },
    },
    Paths: map[string]chioas.Path{
        "/people": {
            Methods: map[string]chioas.Method{
                http.MethodGet: {
                    Handler: "GetPeople",
                    Responses: chioas.Responses{
                        http.StatusOK: {
                            IsArray:   true,
                            SchemaRef: "Person",
                        },
                    },
                },
                http.MethodPost: {
                    Handler: "AddPerson",
                    Request: &chioas.Request{
                        Schema: personSchema,
                    },
                    Responses: chioas.Responses{
                        http.StatusOK: {
                            SchemaRef: "Person",
                        },
                    },
                },
            },
        },
    },
    Components: &chioas.Components{
        Schemas: chioas.Schemas{
            personSchema,
        },
    },
}

type api struct {
    chioas.Definition
}

var myApi = &api{
    Definition: def,
}

func (a *api) SetupRoutes(r chi.Router) error {
    return a.Definition.SetupRoutes(r, a)
}

type Person struct {
    Id   int    `json:"id" oas:"description:The id of the person,example"`
    Name string `json:"name" oas:"description:The name of the person,example"`
}

var personSchema = (&chioas.Schema{
    Name: "Person",
}).Must(Person{
    Id:   1,
    Name: "Bilbo",
})

// GetPeople is the typed handler for GET /people - note the typed return args
func (a *api) GetPeople() ([]Person, error) {
    return []Person{
        {
            Id:   0,
            Name: "Me",
        },
        {
            Id:   1,
            Name: "You",
        },
    }, nil
}

// AddPerson is the typed handler for POST /person - note the typed input and return args
func (a *api) AddPerson(person *Person) (Person, error) {
    return *person, nil
}

func main() {
    router := chi.NewRouter()
    if err := myApi.SetupRoutes(router); err != nil {
        panic(err)
    }
    _ = http.ListenAndServe(":8080", router)
}

Documentation

Overview

Package typed provides for the capability of having typed handlers (methods/functions) for api endpoints

to utilise typed handlers, just set the MethodHandlerBuilder in chioas.Definition

For full documentation - see https://github.com/go-andiamo/chioas/blob/main/typed/README.md

Example:

package main

import (
	"github.com/go-andiamo/chioas"
	"github.com/go-andiamo/chioas/typed"
	"github.com/go-chi/chi/v5"
	"net/http"
)

var def = chioas.Definition{
	DocOptions: chioas.DocOptions{
		ServeDocs: true,
	},
	MethodHandlerBuilder: typed.NewTypedMethodsHandlerBuilder(nil),
	Methods: map[string]chioas.Method{
		http.MethodGet: {
			Handler: func() (map[string]any, error) {
				return map[string]any{
					"root": "root discovery",
				}, nil
			},
		},
	},
	Paths: map[string]chioas.Path{
		"/people": {
			Methods: map[string]chioas.Method{
				http.MethodGet: {
					Handler: "GetPeople",
					Responses: chioas.Responses{
						http.StatusOK: {
							IsArray:   true,
							SchemaRef: "Person",
						},
					},
				},
				http.MethodPost: {
					Handler: "AddPerson",
					Request: &chioas.Request{
						Schema: personSchema,
					},
					Responses: chioas.Responses{
						http.StatusOK: {
							SchemaRef: "Person",
						},
					},
				},
			},
		},
	},
	Components: &chioas.Components{
		Schemas: chioas.Schemas{
			personSchema,
		},
	},
}

type api struct {
	chioas.Definition
}

var myApi = &api{
	Definition: def,
}

func (a *api) SetupRoutes(r chi.Router) error {
	return a.Definition.SetupRoutes(r, a)
}

type Person struct {
	Id   int    `json:"id" oas:"description:The id of the person,example"`
	Name string `json:"name" oas:"description:The name of the person,example"`
}

var personSchema = (&chioas.Schema{
	Name: "Person",
}).Must(Person{
	Id:   1,
	Name: "Bilbo",
})

// GetPeople is the typed handler for GET /people - note the typed return args
func (a *api) GetPeople() ([]Person, error) {
	return []Person{
		{
			Id:   0,
			Name: "Me",
		},
		{
			Id:   1,
			Name: "You",
		},
	}, nil
}

// AddPerson is the typed handler for POST /person - note the typed input and return args
func (a *api) AddPerson(person *Person) (Person, error) {
	return *person, nil
}

func main() {
	router := chi.NewRouter()
	if err := myApi.SetupRoutes(router); err != nil {
		panic(err)
	}
	_ = http.ListenAndServe(":8080", router)
}

Index

Constants

This section is empty.

Variables

This section is empty.

Functions

func NewTypedMethodsHandlerBuilder

func NewTypedMethodsHandlerBuilder(options ...any) chioas.MethodHandlerBuilder

NewTypedMethodsHandlerBuilder creates a new handler for use on chioas.Definition and provides capability to have typed methods/funcs for API endpoints.

the options arg can be any of types ErrorHandler, Unmarshaler, ResponseHandler, ArgBuilder or ArgExtractor[T]

if no Unmarshaler is passed then a default JSON unmarshaler is used - and if multiple Unmarshaler are passed then only the last one is used

For a complete example, see package docs

Types

type ApiError

type ApiError interface {
	error
	StatusCode() int
	Wrapped() error
}

ApiError is an error interface that can be returned from typed handlers allowing the status code for the error to be set in the response

Implementations of this interface can also be used by ResponseMarshaler.Marshal and JsonResponse.Error

func NewApiError

func NewApiError(statusCode int, msg string) ApiError

NewApiError creates a new ApiError with the specified status code and error message

If the message is an empty string, the actual message is set from the status code using http.StatusText

Note: If the provided status code is less than 100 (http.StatusContinue) the status code http.StatusInternalServerError is used

func NewApiErrorf

func NewApiErrorf(statusCode int, format string, a ...any) ApiError

NewApiErrorf creates a new ApiError with the specified status code and error format + args

If the message is an empty string, the actual message is set from the status code using http.StatusText

Note: If the provided status code is less than 100 (http.StatusContinue) the status code http.StatusInternalServerError is used

func WrapApiError

func WrapApiError(statusCode int, err error) ApiError

WrapApiError creates a new ApiError by wrapping the error and using the provided status code

Note: If the provided error is nil then nil is returned

Note: If the provided status code is less than 100 (http.StatusContinue) the status code http.StatusInternalServerError is used

func WrapApiErrorMsg

func WrapApiErrorMsg(statusCode int, err error, msg string) ApiError

WrapApiErrorMsg creates a new ApiError by wrapping the error - using the provided status code and optionally overriding the message

Note: If the provided error is nil then nil is returned

Note: If the provided status code is less than 100 (http.StatusContinue) the status code http.StatusInternalServerError is used

type ArgBuilder

type ArgBuilder interface {
	// IsApplicable determines whether this ArgBuilder can handle the given arg reflect.Type
	//
	// If it is applicable, this method should return true - and return readsBody true if it intends to read the request body (as only one arg can read the request body)
	//
	// The method and path are provided for information purposes
	IsApplicable(argType reflect.Type, method string, path string) (is bool, readsBody bool)
	// BuildValue builds the final arg reflect.Value that will be used to call the typed handler
	//
	// If no error is returned, then the reflect.Value returned MUST match the arg type (failure to do so will result in an error response)
	BuildValue(argType reflect.Type, request *http.Request, params []urit.PathVar) (reflect.Value, error)
}

ArgBuilder is an interface that can be passed as an option to NewTypedMethodsHandlerBuilder, allowing support for additional typed handler arg types

func NewMultipartFormArgSupport added in v1.12.6

func NewMultipartFormArgSupport(maxMemory int64, noAutoError bool) ArgBuilder

NewMultipartFormArgSupport creates an arg type builder - for use as an option passed to NewTypedMethodsHandlerBuilder(options ...any)

By adding this as an option to NewTypedMethodsHandlerBuilder, any typed handler with an arg of type *multipart.Form will be supported

If a typed handler has an arg type of *multipart.Form but the request is not `Content-Type=multipart/form-data`, or the request body is nil (or any other error from http.Request.ParseMultipartForm) then the typed handler is not called and an error response of 400 Bad Request is served - unless noAuthError is set, in which case such errors result in a nil *multipart.Form being passed to the typed handler

type ArgExtractor added in v1.14.1

type ArgExtractor[T any] struct {
	// Extract is the function that extracts the arg value from the request
	Extract func(r *http.Request) (T, error)
	// ReadsBody denotes that the arg uses the request body
	ReadsBody bool
}

ArgExtractor is a struct that can be passed as an option to NewTypedMethodsHandlerBuilder and contains a function that extracts the typed arg from the request

For example, if your API had multiple places where an 'id' path param was used - you could alias the id type...

type Id string

and then in the handler...

func DeletePerson(personId Id) {
  ...
}

and create an ArgExtractor to get the id from the path param...

 idExtractor := &ArgExtractor[Id]{
	  Extract: func(r *http.Request) (Id, error) {
	    return Id(chi.URLParam(r, "id")), nil
	  },
	}

and use the ArgExtractor...

var myApiDef = chioas.Definition{
  ...
  MethodHandlerBuilder: typed.NewTypedMethodsHandlerBuilder(idExtractor),
  ...
}

Or, if the extractor for the given arg type never reads the request body, you can simply specify the extractor as a func - with the signature:

func(r *http.Request) (T, error)

where T is the arg type

type BasicAuth added in v1.12.8

type BasicAuth struct {
	Username string
	Password string
	Ok       bool
}

BasicAuth is a type that can be used as a typed handler arg to receive request BasicAuth

If type *typed.BasicAuth is used as the typed handler arg, then the value will be nil if no Authorization header is present on the request or if the Authorization header is not basic auth (i.e. value does not start with "Basic ")

type ErrorHandler

type ErrorHandler interface {
	HandleError(writer http.ResponseWriter, request *http.Request, err error)
}

ErrorHandler is an interface that can be used to write an error to the response

an ErrorHandler can be passed as an option to NewTypedMethodsHandlerBuilder

The original *http.Request is passed so that responses can be written in the required content type (i.e. according to the `Accept` header)

type Headers

type Headers map[string][]string

Headers is a type that can be used as a typed handler arg to receive request headers

type JsonResponse

type JsonResponse struct {
	// Error is the optional error that can be returned
	Error error
	// StatusCode is the status code for the response (if less than 100 Continue then 200 OK is assumed)
	StatusCode int
	// Headers is any headers to be set on the response
	Headers [][2]string
	// Body is any payload for the response body (marshalled to JSON)
	Body any
}

JsonResponse is a struct that can be returned from a typed handler

The response error, status code, headers and body are determined by the properties

type MultiResponseMarshaler added in v1.12.2

type MultiResponseMarshaler struct {
	// Error is the optional error that can be returned
	Error error
	// StatusCode is the status code for the response (if less than 100 Continue then 200 OK is assumed)
	StatusCode int
	// Headers is any headers to be set on the response (note the `Content-Type` header will be set automatically but can be overridden by these)
	Headers [][2]string
	// Body is any payload to be marshaled for the response body
	//
	// If Body is nil the status code will default 204 No Content (i.e. if StatusCode is less than 100)
	//
	// Note: It is up to you to ensure that the Body can be reliably marshalled into all json, yaml and xml - for example, map[string]any will NOT marshal into xml
	Body any
	// FallbackContentType is the content type to assume if the `Accept` header is not one of json, yaml or xml
	//
	// If this value is empty (or not one of "application/json", "application/yaml" or "application/xml") then
	// no fallback is used
	FallbackContentType string
	// ExcludeJson if set, prevents MultiResponseMarshaler supporting json
	ExcludeJson bool
	// ExcludeYaml if set, prevents MultiResponseMarshaler supporting yaml
	ExcludeYaml bool
	// ExcludeXml if set, prevents MultiResponseMarshaler supporting xml
	ExcludeXml bool
}

MultiResponseMarshaler is an implementation of ResponseMarshaler that supports marshalling a response value as json, yaml or xml - according to the request `Accept` header

If the `Accept` header does not indicate json, yaml or xml then an ApiError indicating status code 406 Not Acceptable (unless a FallbackContentType is supplied)

func (*MultiResponseMarshaler) Marshal added in v1.12.2

func (m *MultiResponseMarshaler) Marshal(request *http.Request) (data []byte, statusCode int, hdrs [][2]string, err error)

type PathParams

type PathParams map[string][]string

PathParams is a type that can be used as a typed handler arg to receive request path params

Another way to receive request path params (in order) is to use either []string or ...string (varadic) examples:

func getSomething(pathParams []string) (json.RawMessage, error)

func getSomething(pathParams ..string) (json.RawMessage, error)

type PostForm added in v1.12.7

type PostForm url.Values

PostForm is a type that can be used as a typed handler arg to receive request PostForm values

Note: If this arg type is used for a typed handler that does not handle http methods POST, PUT or PATH - then the value will be empty (and the request body will not have been read)

type QueryParams

type QueryParams map[string][]string

QueryParams is a type that can be used as a typed handler arg to receive request query params

type RawQuery

type RawQuery string

RawQuery is a type that can be used as a typed handler arg to receive request raw query

type ResponseHandler added in v1.16.3

type ResponseHandler interface {
	// WriteResponse writes a successful response
	//
	// the value and statusCode args are obtained from the return args of the typed handler
	WriteResponse(writer http.ResponseWriter, request *http.Request, value any, statusCode int, thisApi any)
	// WriteErrorResponse writes an error response
	//
	// the err arg is obtained from the error returned from the typed handler
	WriteErrorResponse(writer http.ResponseWriter, request *http.Request, err error, thisApi any)
}

ResponseHandler is an interface that can be used to write responses

a ResponseHandler can be passed as an option to NewTypedMethodsHandlerBuilder

The API itself can also implement this interface

type ResponseMarshaler

type ResponseMarshaler interface {
	// Marshal marshals the response for writing
	//
	// the *http.Request is passed as an arg so that the marshalling can take account of request type (i.e. `Accept` header)
	Marshal(request *http.Request) (data []byte, statusCode int, hdrs [][2]string, err error)
}

ResponseMarshaler is an interface that can be implemented by objects returned from a typed handler

The body of the response is written by the data provided from Marshal, the response status code is also set from that returned and so are any headers

type Unmarshaler

type Unmarshaler interface {
	Unmarshal(request *http.Request, v any) error
}

Unmarshaler is an interface that can be passed as an option to NewTypedMethodsHandlerBuilder, allowing support for unmarshalling different content types (e.g. overriding default JSON unmarshalling and/or varying the unmarshalling according to the request `Content-Type` header)

var (
	MultiUnmarshaler Unmarshaler = _MultiUnmarshaler // MultiUnmarshaler is an Unmarshaler that supports unmarshalling json, yaml and xml - and can be used as an option passed to NewTypedMethodsHandlerBuilder
)

Jump to

Keyboard shortcuts

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