rest

package module
v0.2.66 Latest Latest
Warning

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

Go to latest
Published: Mar 29, 2024 License: MIT Imports: 11 Imported by: 18

README

REST with Clean Architecture for Go

Build Status Coverage Status GoDevDoc Time Tracker Code lines Comments

This module implements HTTP transport level for github.com/swaggest/usecase to build REST services.

Goals

  • Maintain single source of truth for documentation, validation and input/output of HTTP API.
  • Avoid dependency on compile time code generation.
  • Improve productivity and reliability by abstracting HTTP details with simple API for common case.
  • Allow low-level customizations for advanced cases.
  • Maintain reasonable performance with low GC impact.

Non-Goals

  • Support for legacy documentation schemas like Swagger 2.0 or RAML.
  • Zero allocations.
  • Explicit support for XML in request or response bodies.

Features

Usage

Please check this tutorial for end-to-end usage example.

Request Decoder

Go struct with field tags defines input port. Request decoder populates field values from http.Request data before use case interactor is invoked.

// Declare input port type.
type helloInput struct {
    Locale string `query:"locale" default:"en-US" pattern:"^[a-z]{2}-[A-Z]{2}$" enum:"ru-RU,en-US"`
    Name   string `path:"name" minLength:"3"` // Field tags define parameter location and JSON schema constraints.

    // Field tags of unnamed fields are applied to parent schema, 
	// they are optional and can be used to disallow unknown parameters.
    // For non-body params, name tag must be provided explicitly.
    // E.g. here no unknown `query` and `cookie` parameters allowed,
    // unknown `header` params are ok.
    _ struct{} `query:"_" cookie:"_" additionalProperties:"false"`
}

Input data can be located in:

  • path parameter in request URI, e.g. /users/{name},
  • query parameter in request URI, e.g. /users?locale=en-US,
  • formData parameter in request body with application/x-www-form-urlencoded or multipart/form-data content,
  • form parameter acts as formData or query,
  • json parameter in request body with application/json content,
  • cookie parameter in request cookie,
  • header parameter in request header.

For more explicit separation of concerns between use case and transport it is possible to provide request mapping separately when initializing handler (please note, such mapping is not applied to json body).

// Declare input port type.
type helloInput struct {
    Locale string `default:"en-US" pattern:"^[a-z]{2}-[A-Z]{2}$"`
    Name   string `minLength:"3"` // Field tags define parameter location and JSON schema constraints.
}
// Add use case handler with custom input mapping to router.
r.Method(http.MethodGet, "/hello/{name}", nethttp.NewHandler(u,
    nethttp.RequestMapping(new(struct {
       Locale string `query:"locale"`
       Name   string `path:"name"` // Field tags define parameter location and JSON schema constraints.
    })),
))

Additional field tags describe JSON schema constraints, please check documentation.

More schema customizations are possible with github.com/swaggest/jsonschema-go interfaces.

By default default tags are only contributing to documentation, if request.DecoderFactory.ApplyDefaults is set to true, fields of request structure that don't have explicit value but have default will be populated with default value.

If input structure implements request.Loader,
then LoadFromHTTPRequest(r *http.Request) error method will be invoked to populate input structure instead of automatic decoding. This allows low level control for cases that need it.

Request decoder can be used standalone, in already existing `ServeHTTP`.
type MyRequest struct {
    Foo int    `header:"X-Foo"`
    Bar string `formData:"bar"`
    Baz bool   `query:"baz"`
}

// A decoder for particular structure, can be reused for multiple HTTP requests.
myDecoder := request.NewDecoderFactory().MakeDecoder(http.MethodPost, new(MyRequest), nil)

// Request and response writer from ServeHTTP.
var (
    rw  http.ResponseWriter
    req *http.Request
)

// This code would presumably live in ServeHTTP.
var myReq MyRequest

if err := myDecoder.Decode(req, &myReq, nil); err != nil {
    http.Error(rw, err.Error(), http.StatusBadRequest)
}
Response Encoder

Go struct with field tags defines output port. Response encoder writes data from output to http.ResponseWriter after use case interactor invocation finishes.

// Declare output port type.
type helloOutput struct {
    Now     time.Time `header:"X-Now" json:"-"`
    Message string    `json:"message"`
    Sess    string    `cookie:"sess,httponly,secure,max-age=86400,samesite=lax"`
}

Output data can be located in:

  • json for response body with application/json content,
  • header for values in response header,
  • cookie for cookie values, cookie fields can have configuration in field tag (same as in actual cookie, but with comma separation).

For more explicit separation of concerns between use case and transport it is possible to provide response header mapping separately when initializing handler.

// Declare output port type.
type helloOutput struct {
    Now     time.Time `json:"-"`
    Message string    `json:"message"`
}
// Add use case handler with custom output headers mapping to router.
r.Method(http.MethodGet, "/hello/{name}", nethttp.NewHandler(u,
    nethttp.ResponseHeaderMapping(new(struct {
        Now     time.Time `header:"X-Now"`
    })),
))

Additional field tags describe JSON schema constraints, please check documentation.

Creating Use Case Interactor

HTTP transport is decoupled from business logic by adapting use case interactors.

Use case interactor can define input and output ports that are used to map data between Go values and transport. It can provide information about itself that will be exposed in generated documentation.

// Create use case interactor with references to input/output types and interaction function.
u := usecase.NewInteractor(func(ctx context.Context, input helloInput, output *helloOutput) error {
    msg, available := messages[input.Locale]
    if !available {
        return status.Wrap(errors.New("unknown locale"), status.InvalidArgument)
    }

    output.Message = fmt.Sprintf(msg, input.Name)
    output.Now = time.Now()

    return nil
})
Initializing Web Service

Web Service is an instrumented facade in front of router, it simplifies configuration and provides more compact API to add use cases.

// Service initializes router with required middlewares.
service := web.NewService(openapi31.NewReflector())

// It allows OpenAPI configuration.
service.OpenAPISchema().SetTitle("Albums API")
service.OpenAPISchema().SetDescription("This service provides API to manage albums.")
service.OpenAPISchema().SetVersion("v1.0.0")

// Additional middlewares can be added.
service.Use(
    middleware.StripSlashes,

    // cors.AllowAll().Handler, // "github.com/rs/cors", 3rd-party CORS middleware can also be configured here.
)

// Use cases can be mounted using short syntax .<Method>(...).
service.Post("/albums", postAlbums(), nethttp.SuccessStatus(http.StatusCreated))

log.Println("Starting service at http://localhost:8080")

if err := http.ListenAndServe("localhost:8080", service); err != nil {
    log.Fatal(err)
}

Usually, web.Service API is sufficient, but if it is not, router can be configured manually, please check the documentation below.

Security Setup

Example with HTTP Basic Auth.

// Prepare middleware with suitable security schema.
// It will perform actual security check for every relevant request.
adminAuth := middleware.BasicAuth("Admin Access", map[string]string{"admin": "admin"})

// Prepare API schema updater middleware.
// It will annotate handler documentation with security schema.
adminSecuritySchema := nethttp.HTTPBasicSecurityMiddleware(apiSchema, "Admin", "Admin access")

// Endpoints with admin access.
r.Route("/admin", func(r chi.Router) {
    r.Group(func(r chi.Router) {
        r.Use(adminAuth, adminSecuritySchema) // Add both middlewares to routing group to enforce and document security.
        r.Method(http.MethodPut, "/hello/{name}", nethttp.NewHandler(u))
    })
})

Example with cookie.

// Security middlewares.
//  - sessMW is the actual request-level processor,
//  - sessDoc is a handler-level wrapper to expose docs.
sessMW := func(handler http.Handler) http.Handler {
    return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
        if c, err := r.Cookie("sessid"); err == nil {
            r = r.WithContext(context.WithValue(r.Context(), "sessionID", c.Value))
        }

        handler.ServeHTTP(w, r)
    })
}

sessDoc := nethttp.APIKeySecurityMiddleware(s.OpenAPICollector, "User",
    "sessid", oapi.InCookie, "Session cookie.")

// Security schema is configured for a single top-level route.
s.With(sessMW, sessDoc).Method(http.MethodGet, "/root-with-session", nethttp.NewHandler(dummy()))

// Security schema is configured on a sub-router.
s.Route("/deeper-with-session", func(r chi.Router) {
    r.Group(func(r chi.Router) {
        r.Use(sessMW, sessDoc)

        r.Method(http.MethodGet, "/one", nethttp.NewHandler(dummy()))
        r.Method(http.MethodGet, "/two", nethttp.NewHandler(dummy()))
    })
})

See example.

Handler Setup

Handler is a generalized adapter for use case interactor, so usually setup is trivial.

// Add use case handler to router.
r.Method(http.MethodGet, "/hello/{name}", nethttp.NewHandler(u))

Example

For non-generic use case, see another example.

package main

import (
	"context"
	"errors"
	"fmt"
	"log"
	"net/http"
	"time"

	"github.com/swaggest/openapi-go/openapi31"
	"github.com/swaggest/rest/response/gzip"
	"github.com/swaggest/rest/web"
	swgui "github.com/swaggest/swgui/v5emb"
	"github.com/swaggest/usecase"
	"github.com/swaggest/usecase/status"
)

func main() {
	s := web.NewService(openapi31.NewReflector())

	// Init API documentation schema.
	s.OpenAPISchema().SetTitle("Basic Example")
	s.OpenAPISchema().SetDescription("This app showcases a trivial REST API.")
	s.OpenAPISchema().SetVersion("v1.2.3")

	// Setup middlewares.
	s.Wrap(
		gzip.Middleware, // Response compression with support for direct gzip pass through.
	)

	// Declare input port type.
	type helloInput struct {
		Locale string `query:"locale" default:"en-US" pattern:"^[a-z]{2}-[A-Z]{2}$" enum:"ru-RU,en-US"`
		Name   string `path:"name" minLength:"3"` // Field tags define parameter location and JSON schema constraints.

		// Field tags of unnamed fields are applied to parent schema.
		// they are optional and can be used to disallow unknown parameters.
		// For non-body params, name tag must be provided explicitly.
		// E.g. here no unknown `query` and `cookie` parameters allowed,
		// unknown `header` params are ok.
		_ struct{} `query:"_" cookie:"_" additionalProperties:"false"`
	}

	// Declare output port type.
	type helloOutput struct {
		Now     time.Time `header:"X-Now" json:"-"`
		Message string    `json:"message"`
	}

	messages := map[string]string{
		"en-US": "Hello, %s!",
		"ru-RU": "Привет, %s!",
	}

	// Create use case interactor with references to input/output types and interaction function.
	u := usecase.NewInteractor(func(ctx context.Context, input helloInput, output *helloOutput) error {
		msg, available := messages[input.Locale]
		if !available {
			return status.Wrap(errors.New("unknown locale"), status.InvalidArgument)
		}

		output.Message = fmt.Sprintf(msg, input.Name)
		output.Now = time.Now()

		return nil
	})

	// Describe use case interactor.
	u.SetTitle("Greeter")
	u.SetDescription("Greeter greets you.")

	u.SetExpectedErrors(status.InvalidArgument)

	// Add use case handler to router.
	s.Get("/hello/{name}", u)

	// Swagger UI endpoint at /docs.
	s.Docs("/docs", swgui.New)

	// Start server.
	log.Println("http://localhost:8011/docs")
	if err := http.ListenAndServe("localhost:8011", s); err != nil {
		log.Fatal(err)
	}
}

Documentation Page

Additional Integrations

Performance Optimization

If top performance is critical for the service or particular endpoints, you can trade simplicity for performance by implementing manual request loader on input type.

func (i *myInput) LoadFromHTTPRequest(r *http.Request) (err error) {
	i.Header = r.Header.Get("X-Header")

	return nil
}

If request.Loader is implemented, it will be called instead of both automatic decoding and validation.

Check advanced example.

To further improve performance you may try to use fasthttp instead of net/http with rest-fasthttp fork.

Versioning

This project adheres to Semantic Versioning.

Before version 1.0.0, breaking changes are tagged with MINOR bump, features and fixes are tagged with PATCH bump. After version 1.0.0, breaking changes are tagged with MAJOR bump.

Breaking changes are described in UPGRADE.md.

Advanced Usage

Advanced Usage

Documentation

Overview

Package rest provides http handler for use case interactor to implement REST API.

Index

Constants

View Source
const (
	// ParamInPath indicates path parameters, such as `/users/{id}`.
	ParamInPath = ParamIn("path")

	// ParamInQuery indicates query parameters, such as `/users?page=10`.
	ParamInQuery = ParamIn("query")

	// ParamInBody indicates body value, such as `{"id": 10}`.
	ParamInBody = ParamIn("body")

	// ParamInFormData indicates body form parameters.
	ParamInFormData = ParamIn("formData")

	// ParamInCookie indicates cookie parameters, which are passed ParamIn the `Cookie` header,
	// such as `Cookie: debug=0; gdpr=2`.
	ParamInCookie = ParamIn("cookie")

	// ParamInHeader indicates header parameters, such as `X-Header: value`.
	ParamInHeader = ParamIn("header")
)

Variables

This section is empty.

Functions

func HTTPStatusFromCanonicalCode

func HTTPStatusFromCanonicalCode(c status.Code) int

HTTPStatusFromCanonicalCode returns http status accordingly to use case status code.

func OutputHasNoContent

func OutputHasNoContent(output interface{}) bool

OutputHasNoContent indicates if output does not seem to have any content body to render in response.

Types

type ETagged

type ETagged interface {
	ETag() string
}

ETagged exposes specific version of resource.

type ErrResponse

type ErrResponse struct {
	StatusText string                 `json:"status,omitempty" description:"Status text."`
	AppCode    int                    `json:"code,omitempty" description:"Application-specific error code."`
	ErrorText  string                 `json:"error,omitempty" description:"Error message."`
	Context    map[string]interface{} `json:"context,omitempty" description:"Application context."`
	// contains filtered or unexported fields
}

ErrResponse is HTTP error response body.

func Err

func Err(err error) (int, ErrResponse)

Err creates HTTP status code and ErrResponse for error.

You can use it with use case status code:

rest.Err(status.NotFound)

func (ErrResponse) Error

func (e ErrResponse) Error() string

Error implements error.

func (ErrResponse) Unwrap

func (e ErrResponse) Unwrap() error

Unwrap returns parent error.

type ErrWithAppCode

type ErrWithAppCode interface {
	error
	AppErrCode() int
}

ErrWithAppCode exposes application error code.

type ErrWithCanonicalStatus

type ErrWithCanonicalStatus interface {
	error
	Status() status.Code
}

ErrWithCanonicalStatus exposes canonical status code.

type ErrWithFields

type ErrWithFields interface {
	error
	Fields() map[string]interface{}
}

ErrWithFields exposes structured context of error.

type ErrWithHTTPStatus

type ErrWithHTTPStatus interface {
	error
	HTTPStatus() int
}

ErrWithHTTPStatus exposes HTTP status code.

type HTTPCodeAsError added in v0.2.43

type HTTPCodeAsError int

HTTPCodeAsError exposes HTTP status code as use case error that can be translated to response status.

func (HTTPCodeAsError) Error added in v0.2.43

func (c HTTPCodeAsError) Error() string

Error return HTTP status text.

func (HTTPCodeAsError) HTTPStatus added in v0.2.43

func (c HTTPCodeAsError) HTTPStatus() int

HTTPStatus returns HTTP status code.

type HandlerTrait

type HandlerTrait struct {
	// SuccessStatus is an HTTP status code to set on successful use case interaction.
	//
	// Default is 200 (OK) or 204 (No Content).
	SuccessStatus int

	// SuccessContentType is a Content-Type of successful response, default application/json.
	SuccessContentType string

	// MakeErrResp overrides error response builder instead of default Err,
	// returned values are HTTP status code and error structure to be marshaled.
	MakeErrResp func(ctx context.Context, err error) (int, interface{})

	// ReqMapping controls request decoding into use case input.
	// Optional, if not set field tags are used as mapping.
	ReqMapping RequestMapping

	RespHeaderMapping map[string]string
	RespCookieMapping map[string]http.Cookie

	// ReqValidator validates decoded request data.
	ReqValidator Validator

	// RespValidator validates decoded response data.
	RespValidator Validator

	// OperationAnnotations are called after operation setup and before adding operation to documentation.
	//
	// Deprecated: use OpenAPIAnnotations.
	OperationAnnotations []func(op *openapi3.Operation) error

	// OpenAPIAnnotations are called after operation setup and before adding operation to documentation.
	OpenAPIAnnotations []func(oc openapi.OperationContext) error
}

HandlerTrait controls basic behavior of rest handler.

func (*HandlerTrait) RequestMapping

func (h *HandlerTrait) RequestMapping() RequestMapping

RequestMapping returns custom mapping for request decoder.

func (*HandlerTrait) RestHandler

func (h *HandlerTrait) RestHandler() *HandlerTrait

RestHandler is an accessor.

type HandlerWithRoute

type HandlerWithRoute interface {
	// RouteMethod returns http method of action.
	RouteMethod() string

	// RoutePattern returns http path pattern of action.
	RoutePattern() string
}

HandlerWithRoute is a http.Handler with routing information.

type HandlerWithUseCase

type HandlerWithUseCase interface {
	UseCase() usecase.Interactor
}

HandlerWithUseCase exposes usecase.

type JSONSchemaValidator

type JSONSchemaValidator interface {
	Validator

	// AddSchema accepts JSON schema for a request parameter or response value.
	AddSchema(in ParamIn, name string, schemaData []byte, required bool) error
}

JSONSchemaValidator defines JSON schema validator.

type JSONWriterTo

type JSONWriterTo interface {
	JSONWriteTo(w io.Writer) (int, error)
}

JSONWriterTo writes JSON payload.

type OutputWithHTTPStatus added in v0.2.47

type OutputWithHTTPStatus interface {
	HTTPStatus() int
	ExpectedHTTPStatuses() []int
}

OutputWithHTTPStatus exposes HTTP status code(s) for output.

type ParamIn

type ParamIn string

ParamIn defines parameter location.

type RequestErrors

type RequestErrors map[string][]string

RequestErrors is a list of validation or decoding errors.

Key is field position (e.g. "path:id" or "body"), value is a list of issues with the field.

func (RequestErrors) Error

func (re RequestErrors) Error() string

Error returns error message.

func (RequestErrors) Fields

func (re RequestErrors) Fields() map[string]interface{}

Fields returns request errors by field location and name.

type RequestJSONSchemaProvider

type RequestJSONSchemaProvider interface {
	ProvideRequestJSONSchemas(
		method string,
		input interface{},
		mapping RequestMapping,
		validator JSONSchemaValidator,
	) error
}

RequestJSONSchemaProvider provides request JSON Schemas.

type RequestMapping

type RequestMapping map[ParamIn]map[string]string

RequestMapping describes how decoded request should be applied to container struct.

It is defined as a map by parameter location. Each item is a map with struct field name as key and decoded field name as value.

Example:

map[rest.ParamIn]map[string]string{rest.ParamInQuery:map[string]string{"ID": "id", "FirstName": "first-name"}}

type RequestValidatorFactory

type RequestValidatorFactory interface {
	MakeRequestValidator(method string, input interface{}, mapping RequestMapping) Validator
}

RequestValidatorFactory creates request validator for particular structured Go input value.

type ResponseJSONSchemaProvider

type ResponseJSONSchemaProvider interface {
	ProvideResponseJSONSchemas(
		statusCode int,
		contentType string,
		output interface{},
		headerMapping map[string]string,
		validator JSONSchemaValidator,
	) error
}

ResponseJSONSchemaProvider provides response JSON Schemas.

type ResponseValidatorFactory

type ResponseValidatorFactory interface {
	MakeResponseValidator(
		statusCode int,
		contentType string,
		output interface{},
		headerMapping map[string]string,
	) Validator
}

ResponseValidatorFactory creates response validator for particular structured Go output value.

type ValidationErrors

type ValidationErrors map[string][]string

ValidationErrors is a list of validation errors.

Key is field position (e.g. "path:id" or "body"), value is a list of issues with the field.

func (ValidationErrors) Error

func (re ValidationErrors) Error() string

Error returns error message.

func (ValidationErrors) Fields

func (re ValidationErrors) Fields() map[string]interface{}

Fields returns request errors by field location and name.

type Validator

type Validator interface {
	// ValidateData validates decoded request/response data and returns error in case of invalid data.
	ValidateData(in ParamIn, namedData map[string]interface{}) error

	// ValidateJSONBody validates JSON encoded body and returns error in case of invalid data.
	ValidateJSONBody(jsonBody []byte) error

	// HasConstraints indicates if there are validation rules for parameter location.
	HasConstraints(in ParamIn) bool
}

Validator validates a map of decoded data.

type ValidatorFunc

type ValidatorFunc func(in ParamIn, namedData map[string]interface{}) error

ValidatorFunc implements Validator with a func.

func (ValidatorFunc) HasConstraints added in v0.1.3

func (v ValidatorFunc) HasConstraints(_ ParamIn) bool

HasConstraints indicates if there are validation rules for parameter location.

func (ValidatorFunc) ValidateData

func (v ValidatorFunc) ValidateData(in ParamIn, namedData map[string]interface{}) error

ValidateData implements Validator.

func (ValidatorFunc) ValidateJSONBody

func (v ValidatorFunc) ValidateJSONBody(body []byte) error

ValidateJSONBody implements Validator.

Directories

Path Synopsis
_examples module
Package chirouter provides instrumentation for chi.Router.
Package chirouter provides instrumentation for chi.Router.
Package gorillamux provides OpenAPI docs collector for gorilla/mux web services.
Package gorillamux provides OpenAPI docs collector for gorilla/mux web services.
Package gzip provides pre-compressed data container.
Package gzip provides pre-compressed data container.
Package jsonschema implements request validator with github.com/santhosh-tekuri/jsonschema/v2.
Package jsonschema implements request validator with github.com/santhosh-tekuri/jsonschema/v2.
Package nethttp provides instrumentation for net/http.
Package nethttp provides instrumentation for net/http.
Package openapi provides documentation collector.
Package openapi provides documentation collector.
Package request implements reflection-based net/http request decoder.
Package request implements reflection-based net/http request decoder.
Package response implements reflection-based net/http response encoder.
Package response implements reflection-based net/http response encoder.
gzip
Package gzip provides http compression support.
Package gzip provides http compression support.
Package resttest provides utilities to test REST API.
Package resttest provides utilities to test REST API.
Package web provides default facades for web service bootstrap.
Package web provides default facades for web service bootstrap.

Jump to

Keyboard shortcuts

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