restapi

package module
v0.3.0 Latest Latest
Warning

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

Go to latest
Published: Jul 4, 2024 License: MIT Imports: 12 Imported by: 0

README

restapi

truly RESTful API endpoint functions

Installation

$ go get github.com/blugnu/restapi

Features

The Problem

Implementing REST API endpoints in Golang can involve a lot of boilerplate code to handle HTTP responses correctly. This can result in code that is harder to read and maintain and may even lead to incorrect responses if the correct order of operations is not followed when writing response headers.

func (h *Handler) Post(w http.ResponseWriter, r *http.Request) {
    type data struct {
        ID      int    `json:"id"`
        Name    string `json:"name"`
        Surame  string `json:"surname"`
    }

    // Parse request body
    var person data
    if err := json.NewDecoder(r.Body).Decode(&person); err != nil {
        w.WriteHeader(http.StatusBadRequest)
        w.Write([]byte("invalid request body"))
        return
    }

    // Validate request body
    if person.Name == "" || person.Surname == "" {
        w.WriteHeader(http.StatusBadRequest)
        w.Write([]byte("missing name or surname"))
        return
    }

    // Store data in database
    data.ID, err := h.db.Store(person)
    if err != nil {
        w.WriteHeader(http.StatusInternalServerError)
        w.Write([]byte(err.Error()))
        return
    }

    // Marshal data to JSON
    body, err := json.Marshal(data)
    if err != nil {
        w.Write([]byte(err.Error())) // WRONG: status code not set; will incorrectly respond with 200 OK
        return
    }

    // Write response
    w.WriteHeader(http.StatusCreated)
    w.Header().Set("Content-Type", "application/json") // WRONG: this must be applied before calling WriteHeader
    w.Write(body)
}

The Solution

The restapi package simplifies the implementation of REST API endpoints in Golang. The package provides a HandleRequest function to simplify the handling of request bodies, and middleware that takes care of the routine work of marshalling content and writing responses.

Combined, these allow your endpoint functions to focus on and express the concerns of your API domain.

With restapi the above example could be rewritten as follows:

import "github.com/blugnu/restapi"

func (h *Handler) Post(ctx context.Context, r *http.Request) any {
    type data struct {
        ID       int    `json:"id"`
        Name     string `json:"name"`
        Surname  string `json:"surname"`
    }
    return restapi.HandleRequest(r, func(person *data) any {
        if data == nil {
            return restapi.BadRequest("missing request body")
        }

        // Validate request body
        if person.Name == "" || person.Surname == "" {
            return restapi.BadRequest("missing name or surname")
        }

        // Store data in database
        data.ID, err := h.db.Store(person)
        if err != nil {
            return err
        }

        return restapi.Created().WithValue(data)
    })
}

Simplified Unit Tests

In addition to simplifying the implementation of endpoint functions themselves, unit tests for those functions are also simplified. Instead of testing indirectly by establishing a recorder and laboriously testing the response, you can test the endpoint function directly:

Example Unit Test (using net/http/httptest package)

func TestPost(t *testing.T) {
    h := &Handler{ db: &MockDB{} }
    r := httptest.NewRequest(http.MethodPost, "/post", strings.NewReader(`{"name":"John","surname":"Doe"}`))
    w := httptest.NewRecorder()

    // Call the endpoint function directly
    h.Post(w, r)

    // Check the response
    if w.Code != http.StatusCreated {
        t.Errorf("expected: %d\ngot     : %d", http.StatusCreated, w.Code)
    }
    if w.Header().Get("Content-Type") != "application/json" {
        t.Errorf("expected: %s\ngot     : %s", "application/json", w.Header().Get("Content-Type"))
    }
    wanted := `{"id":1,"name":"John","surname":"Doe"}`
    if w.Body.String() != wanted {
        t.Errorf("expected: %s\ngot     : %s", wanted, w.Body.String())
    }
}

Example Unit Test (using restapi package)

import "github.com/blugnu/restapi"

func TestPost(t *testing.T) {
    // ARRANGE
    ctx := context.Background()
    h := &Handler{ db: &MockDB{} }
    r := httptest.NewRequest(http.MethodPost, "/post", strings.NewReader(`{"name":"John","surname":"Doe"}`))

    // ACT
    result := h.Post(ctx, r)

    // ASSERT
    // (also illustrates tests using the `github.com/blugnu/test` package)
    test.That(t, result).Equals(&restapi.Result {
        Status: http.StatusCreated,
        ContentType: "application/json",
        Value: &Person{ ID: 1, Name: "John", Surname: "Doe" },
    })
}

Response Generation: How It Works

The restapi package provides an http end-ware restapi.Handler() function which accepts a modified http.HandlerFunc returning an any value:

import "github.com/blugnu/restapi"

func (h *Handler) Get(ctx context.Context, r *http.Request) any {
    // Parse query parameters
    query := r.URL.Query()
    id := query.Get("id")
    if id == "" {
        return restapi.BadRequest("missing id parameter")
    }

    // Fetch data from database
    data, err := h.db.Get(id)
    if err != nil {
        return err
    }

    // Write response
    return data
}

func main() {
    http.Handle("/get", restapi.HandlerFunc(Get))
    http.ListenAndServe(":8080", nil)
}

Note: Although it functions similarly, the restapi.HandlerFunc() function is referred to as 'end-ware' rather than 'middleware'. This is because a restapi.EndpointFunc() signature differs from a http.Handler. As a result, the restapi.Handler is typically placed at the end of any middleware chain (though there may be a "long tail" of restapi.Handler middlewares)

In addition to the HandlerFunc endware, the restapi package also provides a Handler() endware which accepts a restapi.EndpointHandler rather than a function:

import "github.com/blugnu/restapi"

type GetHandler struct {
    db *Database
}

func (h *GetHandler) ServeAPI(ctx context.Context, r *http.Request) any {
    // Fetch data from database
    data, err := h.db.Get(id)
    if err != nil {
        return err
    }

    // Write response
    return data
}

func main() {
    db, err := ConnectDatabase()
    if err != nil {
        log.Fatal(err)
    }
    http.Handle("/get", restapi.Handler(GetHandler{db: db}))
    http.ListenAndServe(":8080", nil)
}

Whether using HandlerFunc() or Handler(), initial checks are performed on each received request to identify and validate any Accept header before calling the supplied endpoint function. An appropriate response is then constructed and written, according to the type of the value returned by the endpoint function:

Result Type Response
error Internal Server Error (see: Error Responses)
*restapi.Error Error Response
*restapi.Problem RFC7807 Problem Details Response
*restapi.Result Result Response
[]byte - Non-empty: 200 OK response (application/octect-stream)
- Empty: 204 No Content
int response with the returned int as HTTP Status Code and no content
<any other type> 200 OK response with value marshalled as content

Result Response

For more control over the response, an endpoint function can return a *restapi.Result value, obtained by calling one of the following functions:

Function Description
Created() a new *Result value with a 201 Created status
NoContent() a new *Result value with a 204 No Content status
OK() a new *Result value with a 200 OK status
Status() a new *Result value with a specified status code

The *Result type provides methods to set additional details for the response:

Method Description
WithContent() Set the content (and content type) of the response
WithHeader()
WithHeaders()
WithNonCanonicalHeader()
Add canonical/non-canonical headers to the response
WithValue() Set the value to be marshalled as the response content

Example Result Response (implicit 200 OK)

import "github.com/blugnu/restapi"

func (h *Handler) Get(ctx context.Context, r *http.Request) any {
    // Parse query parameters
    query := r.URL.Query()
    id := query.Get("id")
    if id == "" {
        return restapi.BadRequest("missing id parameter")
    }

    // Fetch data from database
    data, err := h.db.Get(id)
    if err != nil {
        return err
    }

    // Will yield a 200 OK response with `data` marshalled
    // according to the request `Accept` header
    return data
}

Example Result Response (explicit 202 Accepted)

import "github.com/blugnu/restapi"

func (h *Handler) Put(ctx context.Context, r *http.Request) any {
    // Parse request body
    var data any
    if err := json.NewDecoder(r.Body).Decode(&data); err != nil {
        return restapi.BadRequest("invalid request body")
    }

    // Store data in database asynchronously
    // (illustrates use of github.com/blugnu/ulog for context logging)
    go func () { if err = h.db.Store(data); err != nil {
        ulog.FromContext(rq.Context()).
            Error(ctx, err)
    } }()

    return restapi.Status(http.StatusAccepted)
}

Error Responses

A restapi endpoint function can return an error response by returning an error or an *restapi.Error.

If an error is returned, a 500 Internal Server Error response is generated. For responses with other status codes, an *restapi.Error value should be returned, obtained by calling one of the following functions:

  • NewError()
  • BadRequest()
  • Forbidden()
  • InternalServerError()
  • NotFound()
  • Unauthorized()

All of these functions accept an optional set of any arguments and return an *Error value. The arguments are applied according to type as follows:

  • NewError() only: the first of any int values is used as the HTTP Error.Status code (any additional int values are ignored)
  • string values are concatenated with spaces as the Error.Message
  • if one error value is provided, it is used as the Error.Err
  • if multiple error values are provided, then Error.Err will be the result of errors.Join() on the provided errors

NOTE: int arguments are ignored by all functions except NewError()

If NewError() is called without any int argument, 500 will be used. The *Error type provides methods that allow additional details to be provided for the error response:

Method Description
WithHeader()
WithHeaders()
WithNonCanonicalHeader()
Add canonical/non-canonical headers to the response
WithHelp() Adds a help message to the response
WithProperty() Adds a key:value property to the response

Error Response Mechanism and Customization

When constructing an error response, the details of a *restapi.Error are passed to the restapi.ProjectError function to be projected onto a response model.

NOTE: the restapi.ProjectError function may be replaced by your application to project an error onto a custom model in order to provide custom error responses. Care should be taken to ensure that the resulting model projected by any replacement function is compatible with the Content-Type marshalling requirements of your API; typically this involves supporting both JSON and XML marshalling

The default implementation of ProjectError returns a value supporting both JSON and XML marshalling, equivalent to:

type struct {
   Status     int              `json:"status" xml:"status"`
   Error      string           `json:"error" xml:"error"`
   Message    string           `json:"message,omitempty" xml:"message,omitempty"`
   Help       string           `json:"help,omitempty" xml:"help,omitempty"`
   Path       string           `json:"path" xml:"path"`
   Query      string           `json:"query,omitempty" xml:"query,omitempty"`
   Timestamp  time.Time        `json:"timestamp" xml:"timestamp"`
   Additional map[string]any   `json:"additional,omitempty" xml:"additional,omitempty"`
}
Field Description
Status The HTTP status code
Error HTTP status text for the Status code
Message a message providing details of the error (if provided)
Help A help message (if provided)
Path The request path
Query The request query string (if any)
Timestamp The time the error occurred (UTC)
Additional Additional properties (if any)
Errors and Messages

If both a Message and one or more errors is associated with a *restapi.Error response, the Message in the response will be formatted to present the Message appended to the error, separated by a : character.

example
    err := errors.New("missing id")
    return restapi.BadRequest(err, "an id must be provided in the url query string")

will yield a response similar to:

{
  "status": 400,
  "error": "Bad Request",
  "message": "missing id: an id must be provided in the url query string",
  "path": "/get",
  "timestamp": "2021-09-01T12:00:00Z"
}
Example JSON Error Response (default)
{
  "status": 400,
  "error": "Bad Request",
  "message": "missing id parameter",
  "path": "/get",
  "timestamp": "2021-09-01T12:00:00Z"
}
Example XML Error Response (default)
<error>
   <status>400</status>
   <error>Bad Request</error>
   <message>missing id parameter</message>
   <path>/get</path>
   <timestamp>2021-09-01T12:00:00Z</timestamp>
</error>

Errors During Error Response Construction

If an error occurs when attempting to an error response, a generic plain/text response is returned with details of the original error and the error that occurred during processing.

Error Logging

Errors might occur during the implementation of a REST API application caused by problems with the implementation of the API itself (as opposed to meaningful error responses intentionally returned by the API).

i.e. if an application provides a custom error projection, errors may occur during the projection or marshalling process that are not meaningful to the client, but are important to the application developer. Similarly, marshalling errors may occur if endpoint functions return complex struct types, especially when implementing XML marshalling.

Such errors will be returned by the API as 500 Internal Server Error responses but it may be helpful to also include them in application logs or even to panic when they occur.

To support this, the restapi package provides a restapi.LogError extension point; this is a function variable initially set to a no-op implementation; an application may replace this with a function that will be called with details of any error that occurs during the processing of a response. The function is called with a restapi.InternalError value:

type InternalError struct {
   Err         error
   Help        string
   Message     string
   Request     *http.Request
   ContentType string
}

NOTE: this does not provide tags to support JSON or XML marshalling; it is intended for use in application logs and should be marshalled according to the requirements of the application log system

RFC7807 Support

NOTE: EXPERIMENTAL

The restapi package provides experimental support for RFC7807 problem details responses.

An RFC7807 Problem Detail response is produced when an endpoint function returns a *restapi.Problem. A *restapi.Problem value can be obtained by calling the restapi.NewProblem() function with details of the problem to be reported.

The *Problem type provides methods to set additional details for the problem response. Only fields that are set will be included in the response.

NOTE: RFC7807 support may be subject to significant change in future versions of the restapi package; support may be removed if adoption of RFC7807 is not deemed sufficient to warrant continuing support.

Documentation

Index

Constants

This section is empty.

Variables

View Source
var (
	ErrBodyRequired            = errors.New("a body is required")
	ErrErrorReadingRequestBody = errors.New("error reading request body")
	ErrInvalidAcceptHeader     = errors.New("no formatter for content type")
	ErrInvalidArgument         = errors.New("invalid argument")
	ErrInvalidOperation        = errors.New("invalid operation")
	ErrInvalidStatusCode       = errors.New("invalid statuscode")
	ErrMarshalErrorFailed      = errors.New("error marshalling an Error response")
	ErrMarshalResultFailed     = errors.New("error marshalling response")
	ErrNoAcceptHeader          = errors.New("no Accept header")
	ErrUnexpectedField         = errors.New("unexpected field")
)
View Source
var LogError = func(InternalError) {}

LogError is called when an error is returned from a restapi.Handler or if an error occurs in an aspect of the restapi implementation itself.

LogError is a function variable with an initial NO-OP implementation, i.e. no log is emitted. Applications should replace the implementation with one that produces an appropriate log using the logger configured in their application.

View Source
var ProjectError = func(err ErrorInfo) any {

	pe := errorResponse{
		XMLName:   xml.Name{Local: "error"},
		Status:    err.StatusCode,
		Error:     http.StatusText(err.StatusCode),
		Message:   err.Message,
		Path:      err.Request.URL.Path,
		Query:     err.Request.URL.RawQuery,
		Timestamp: err.TimeStamp,
		Help:      err.Help,
	}

	switch {
	case pe.Message == "" && err.Err != nil:
		pe.Message = err.Err.Error()

	case pe.Message != "" && err.Err != nil:
		pe.Message = err.Err.Error() + ": " + pe.Message
	}

	if len(err.Properties) > 0 {
		pe.Additional = maps.Clone(err.Properties)
	}

	return pe
}

ProjectError is called when writing an error response to obtain a representation of a REST API Error (the 'projection') to be used as the response body. The function is a variable with a default implementation returning a struct with tags supporting both JSON and XML marshalling:

type struct {
	XMLName    xml.Name       `json:"-"` // omit from JSON; set to "error" in XML
	Status     int            `json:"status" xml:"status"`
	Error      string         `json:"error" xml:"error"`
	Message    string         `json:"message,omitempty" xml:"message,omitempty"`
	Path       string         `json:"path" xml:"path"`
	Query      string         `json:"query" xml:"query"`
	Timestamp  time.Time      `json:"timestamp" xml:"timestamp"`
	Help       string         `json:"help,omitempty" xml:"help,omitempty"`
	Additional map[string]any `json:"additional,omitempty" xml:"additional,omitempty"`
}

Applications may customise the body of error responses by replacing the implementation of this function and returning a custom struct or other type with marshalling support appropriate to the needs of the application.

Functions

func Handler

func Handler(h EndpointHandler) http.HandlerFunc

Handler returns a http.HandlerFunc that calls a restapi.EndpointHandler.

A restapi.EndpointHandler is an interface that defines a ServeAPI method that accepts a context.Context and a *http.Request argument, returning a value of type 'any'.

func HandlerFunc

func HandlerFunc(h func(context.Context, *http.Request) any) http.HandlerFunc

HandlerFunc returns a http.HandlerFunc that calls a REST API endpoint function.

The endpoint function differs from a http handler function in that in addition to accepting http.ResponseWriter and *http.Request arguments, it also returns a value of type 'any'.

The returned value is processed by the Handler function to generate an appropriate response.

Types

type EndpointFunc

type EndpointFunc func(context.Context, *http.Request) any

EndpointFunc is a type for a function that conforms to the EndpointHandler ServeAPI() method. A function with the appropriate signature may be converted to an EndpointHandler by casting to this type.

example

func MyEndpoint(ctx context.Context, rq *http.Request) any {
	// do something
	return result
}

var MyHandler = EndpointFunc(MyEndpoint)

func main() {
	http.HandleFunc("/my-endpoint", restapi.Handler(MyHandler))
	http.ListenAndServe(":8080", nil)
}

func (EndpointFunc) ServeAPI added in v0.2.0

func (f EndpointFunc) ServeAPI(ctx context.Context, rq *http.Request) any

ServeAPI implements the EndpointHandler interface for the EndpointFunc type.

type EndpointHandler

type EndpointHandler interface {
	ServeAPI(context.Context, *http.Request) any
}

EndpointHandler is an interface that defines a ServeAPI method that conforms to the EndpointFunc signature, accepting a context.Context and *http.Request arguments, returning a value of type 'any'.

type Error

type Error struct {
	// contains filtered or unexported fields
}

Error holds details of a REST API error.

The Error type is exported but has no exported members; an endpoint function will usually obtain an Error value using an appropriate factory function, using the exported methods to provide information about the error.

examples

// an unexpected error occurred
if err := SomeOperation(); err != nil {
    return restapi.InternalServerError(fmt.Errorf("SomeOperation: %w", err))
}

// an error occurred due to invalid input; provide guidance to the user
if err := GetIDFromRequest(rq, &d); err != nil {
    return restapi.BadRequest().
        WithMessage("ID is missing or invalid").
        WithHelp("The ID must be a valid UUID provided in the request path: /v1/resource/<ID>")

URL         // the URL of the request that resulted in the error
TimeStamp   // the (UTC) time that the error occurred

The following additional information may also be provided by a Handler when returning an Error:

Message     // a message to be displayed with the error.  If not provided,
            // the message will be the string representation of the error (Err).
            //
            // NOTE: if Message is set, the Err string will NOT be used

Help        // a help message to be displayed with the error.  If not provided,
            // the help message will be omitted from the response.

func BadRequest

func BadRequest(args ...any) *Error

BadRequest returns an ApiError with a status code of 400 and the specified error.

func Forbidden

func Forbidden(args ...any) *Error

Forbidden returns an ApiError with a status code of 403 and the specified error.

func InternalServerError

func InternalServerError(args ...any) *Error

InternalServerError returns an ApiError with a status code of 500 and the specified error.

func NewError

func NewError(args ...any) *Error

NewError returns an Error with the specified status code. One or more additional arguments may be provided to be used as follows://

int        // the status code for the error
error      // an error to be wrapped by the Error
string     // a message to be displayed with (or instead of) an error

If no status code is provided http.StatusInternalServerError will be used. If multiple int arguments are provided only the first will be used; any subsequent int arguments will be ignored.

If multiple error arguments are provided they will be wrapped as a single error using errors.Join.

If multiple string arguments are provided, the first non-empty string will be used as the message; any remaining strings will be ignored.

The returned Error will have a timestamp set to the current time in UTC.

panics

NewError will panic with the following errors:

  • ErrInvalidArgument if arguments of an unsupported type are provided.
  • ErrInvalidStatusCode if a status code is provided that is not in the range 4xx-5xx.

examples

// no error occurred, but the operation was not successful
return NewError(http.StatusNotFound, "no document exists with that ID")

// an error occurred, but the error is not relevant to the user
id, err := GetRequestID(rq)
if err != nil {
    return NewError(http.BadRequest, "required document ID is missing or invalid", err)
}

func NotFound

func NotFound(args ...any) *Error

NotFound returns an ApiError with a status code of 404 and the specified error.

func Unauthorized

func Unauthorized(args ...any) *Error

Unauthorized returns an ApiError with a status code of 401 and the specified error.

func (Error) Error

func (err Error) Error() string

Error implements the error interface for an Error, returning a simplified string representation of the error in the form:

<status code> <status>[: error][: message]

where <status> is the http status text associated with <status code>; <error> and <message> are only included if they are set on the Error.

func (Error) Is added in v0.2.0

func (err Error) Is(target error) bool

Is returns true if the target error is an Error and the Error matches the target error. An Error matches the target error if:

  • the status codes match or the target has no status code (matches any);
  • the messages match or the target has no message (matches any);
  • the help messages match or the target has no help message (matches any);
  • the properties match or the target has no properties (matches any);
  • the wrapped target error satisfies errors.Is() or the target has no wrapped error (matches any).

func (Error) String

func (h Error) String() string

func (Error) Unwrap

func (apierr Error) Unwrap() error

Unwrap returns the error wrapped by the Error (or nil).

func (*Error) WithHeader

func (err *Error) WithHeader(k string, v any) *Error

WithHeader sets a header to be included in the response for the error.

The specified header will be added to any headers already set on the Error. If the specified header is already set on the Error the existing header will be replaced with the new value.

The header key is canonicalised using http.CanonicalHeaderKey. To set a header with a non-canonical key use WithNonCanonicalHeader.

func (*Error) WithHeaders

func (err *Error) WithHeaders(headers map[string]any) *Error

WithHeaders sets the headers to be included in the response for the error.

The specified headers will be added to any headers already set on the Error. If the new headers contain values already set on the Error the existing headers will be replaced with the new values.

The header keys are canonicalised using http.CanonicalHeaderKey. To set a header with a non-canonical key use WithNonCanonicalHeader.

func (*Error) WithHelp

func (err *Error) WithHelp(s string) *Error

WithHelp sets the help message for the error.

func (*Error) WithMessage

func (err *Error) WithMessage(s string) *Error

WithMessage sets the message for the error.

func (*Error) WithNonCanonicalHeader

func (err *Error) WithNonCanonicalHeader(k string, v any) *Error

WithNonCanonicalHeader sets a non-canonical header to be included in the response for the error.

The specified header will be added to any headers already set on the Error. If the specified header is already set on the Error the existing header will be replaced with the new value.

The header key is not canonicalised; if the specified key is canonical then the canonical header will be set.

WithNonCanonicalHeader should only be used when a non-canonical header key is specifically required (which is rare). Ordinarily WithHeader should be used.

func (*Error) WithProperty

func (err *Error) WithProperty(key string, value any) *Error

WithProperty sets a property for the error.

type ErrorInfo

type ErrorInfo struct {
	StatusCode int
	Err        error
	Help       string
	Message    string
	Request    *http.Request
	Properties map[string]any
	TimeStamp  time.Time
}

ErrorInfo represents an error that occurred during the processing of a request.

Although it is exported this type should not be used directly by REST API implementations, except when providing an implementation for the restapi.LogError or restapi.ProjectError functions. These functions receive a copy of the Error to be logged or projected in the form of an ErrorInfo.

type InternalError

type InternalError struct {
	Err         error
	Help        string
	Message     string
	Request     *http.Request
	ContentType string
}

InternalError represents an error that occurred during the processing of a request.

Although it is exported this type should not be used directly by REST API implementations, except when providing an implementation for the restapi.LogError or restapi.ProjectError functions. These functions receive a copy of the Error to be logged or projected in the form of an ErrorInfo.

type Problem

type Problem struct {
	Type     *url.URL
	Status   int
	Instance *url.URL
	Detail   string
	Title    string
	// contains filtered or unexported fields
}

Implements an RFC7807 Problem Details response https://www.rfc-editor.org/rfc/rfc7807

func NewProblem

func NewProblem(args ...any) *Problem

NewProblem returns a Problem with the specified arguments. Arguments are processed in order and can be of the following types:

int              // the HTTP status code; will replace any existing Status;
                 // if not specified, defaults to http.StatusInternalServerError

url.URL          // the problem type
*url.URL

string           // the problem detail; will replace any existing detail

error            // will apply a status code of http.StatusInternalServerError and set the
                 // detail to the error message; if the StatusCode or Detail are already
                 // set, they will NOT be overwritten

map[string]any   // additional properties to be included in the response.  If multiple
                 // property maps are specified they will be merged; keys from earlier
                 // arguments will be overwritten by any values for the same key in later
                 // ones

An argument of any other type will cause a panic with ErrInvalidArgument.

If multiple arguments of any of the supported types are specified earlier values in the argument list will be applied and over-written by later values (except as noted above).

examples

// multiple status codes specified: only the last one is applied
NewProblem(http.StatusNotFound, "resource not found", http.BadRequest)

results in a Problem with a StatusCode of 400 (Bad Request) and a Detail of "resource not found"

// status code with multiple errors specified
NewProblem(http.StatusBadRequest, errors.New("some error"), errors.New("another error"))

results in a Problem with a StatusCode of 400 (BadRequest) and a Detail of "some error" (the second error is ignored)

note

Some combinations of arguments may result one or more arguments being ignored. For example, specifying a StatusCode, Detail (string) and an error will result in the error being ignored.

func (*Problem) WithDetail

func (p *Problem) WithDetail(detail string) *Problem

WithDetail sets the Detail property of the Problem instance.

The Detail property must provide a human-readable explanation specific to this occurrence of the problem.

func (*Problem) WithInstance

func (p *Problem) WithInstance(instance url.URL) *Problem

WithInstance sets the instance property of the Problem instance.

The instance property is a URI that identifies the specific occurrence of the problem.

func (*Problem) WithProperty

func (p *Problem) WithProperty(key string, value any) *Problem

func (*Problem) WithType

func (p *Problem) WithType(url url.URL, title ...string) *Problem

WithType sets the Type URL of the Problem instance with an optional Title. If multiple Title values are specified they will be concatenated, with space separators.

type Request

type Request struct {
	*http.Request
	Accept         string
	MarshalContent func(any) ([]byte, error)
}

type Response

type Response struct {
	StatusCode  int
	ContentType string
	Content     []byte
	// contains filtered or unexported fields
}

func (Response) String

func (h Response) String() string

type Result

type Result struct {
	// contains filtered or unexported fields
}

Result holds details of a valid REST API result.

The Result struct is exported but does not export any members; exported methods are provided for endpoint functions to work with a Result when required.

An endpoint function will initialise a Result using one of the provided functions (e.g. OK, Created, etc.) and then set the content and content type and any additional headers if/as required:

examples

// return a 200 OK response with a marshalled struct body
s := resource{ID: "123", Name: "example"}
r := restapi.OK().
    WithValue(s)

// return a 200 OK response with a plain text body
// (ignores/overrides any request Accept header)
r := restapi.OK().
    WithContent("plain/text", []byte("example"))

methods

func Accepted

func Accepted() *Result

Accepted returns a Result with http.StatusAccepted

func Created

func Created() *Result

Created returns a Result with http.StatusCreated

func NoContent

func NoContent() *Result

NoContent returns a Result with http.StatusNoContent

func NotImplemented added in v0.2.0

func NotImplemented() *Result

NotImplemented returns a Result with http.StatusNotImplemented

Strictly speaking this is an Error response (in the 5xx range) but is provided as a Result as it is a common placeholder response for yet-to-be implemented endpoints. Responses of this nature do not require the capabilities of an Error, such as wrapping some runtime error or providing additional Help etc.

func OK

func OK() *Result

OK returns a Result with http.StatusOK

func Status

func Status(statusCode int) *Result

Status returns a Result with the specified status code. The status code must be in the range 1xx-5xx; any other status code will cause a panic.

NOTE:

this is a more strict enforcement of standard HTTP response codes than is applied by WriteHeader itself which, as of May 2024, accepts codes 1xx-9xx.

func (*Result) Content added in v0.3.0

func (r *Result) Content() any

Content returns the content set on the Result.

func (*Result) ContentType added in v0.3.0

func (r *Result) ContentType() string

ContentType returns the content type set on the Result.

func (*Result) Headers added in v0.3.0

func (r *Result) Headers() headers

Headers returns a copy of the headers set on the Result.

func (*Result) StatusCode added in v0.3.0

func (r *Result) StatusCode() int

StatusCode returns the status code set on the Result.

func (Result) String

func (r Result) String() string

String returns a string representation of the Result.

func (*Result) WithContent

func (r *Result) WithContent(contentType string, content []byte) *Result

WithContent sets the content and content type of the Result. The specified content and content type will replace any content or content type that may have been set on the Result previously.

func (*Result) WithHeader

func (r *Result) WithHeader(k string, v any) *Result

WithHeader sets a canonical header on the Result.

The specified header will be added to any headers already set on the Result. If the specified header is already set on the Result the existing header will be replaced with the new value.

The header key is canonicalised using http.CanonicalHeaderKey. To set a header with a non-canonical key use WithNonCanonicalHeader.

func (*Result) WithHeaders

func (r *Result) WithHeaders(headers map[string]any) *Result

WithHeaders sets the headers of the Result.

The specified headers will be added to any headers already set on the Result. If the new headers contain values already set on the Result the existing headers will be replaced with the new values.

The header keys are canonicalised using http.CanonicalHeaderKey. To set a header with a non-canonical key use WithNonCanonicalHeader.

func (*Result) WithNonCanonicalHeader

func (r *Result) WithNonCanonicalHeader(k string, v any) *Result

WithNonCanonicalHeader sets a non-canonical header on the Result.

The specified header will be added to any headers already set on the Result. If the specified header is already set on the Result the existing header will be replaced with the new value.

The header key is not canonicalised; if the specified key is canonical then the canonical header will be set.

WithNonCanonicalHeader should only be used when a non-canonical header key is specifically required (which is rare). Ordinarily WithHeader should be used.

func (*Result) WithValue

func (r *Result) WithValue(value any) *Result

WithValue sets the content of the Result to a value that will be marshalled in the response to the content type indicated in the request Accept header (or restapi.Default.ResponseContentType).

The specified value will replace any content and content type that may have been set on the Result previously.

Directories

Path Synopsis

Jump to

Keyboard shortcuts

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