typed

package
v1.12.6 Latest Latest
Warning

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

Go to latest
Published: Nov 14, 2023 License: Apache-2.0 Imports: 13 Imported by: 0

README

Chioas Typed Handlers

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(req json.RawMessage) req will be the request body read from *http.Request (see also note 1 below)
func eg(req []byte) req will be the request body read from *http.Request (see also note 1 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)

If any of the return args is error (and non-nil) then that error is written to the response writer (see below on how errors are written) - otherwise...

The following rules are applied to decide what is written to http.ResponseWriter:

Return 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)
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)

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 args 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.

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, ArgBuilder, PathParamArgBuilder or Unmarshaler

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 method/func 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 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 handler method/func 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 method/func

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 handler method/func 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 QueryParams

type QueryParams map[string][]string

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

type RawQuery

type RawQuery string

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

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 method/func

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