echopen

package module
v0.0.0-...-5ce3dda Latest Latest
Warning

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

Go to latest
Published: Dec 10, 2023 License: MIT Imports: 15 Imported by: 0

README

TODOs

echOpen

Very thin wrapper around echo to generate OpenAPI v3.1 specs from API endpoints.

This project uses a declarative approach to build an API spec based on the library functions, whilst retaining the full flexibility of the underlying echo engine. This differs from other code-generator approaches such as oapi-codegen which generate a completely flat structure with only global or per-route middleware.

Basic Principle

echOpen defines three levels of wrapper:

  • API: Controls the OpenAPI specification and echo engine
  • Group: Controls echo sub-router groups, with a path prefix, group middleware, default tags, and default security requirements
  • Route: Controls an echo route, which is mounted as an operation within a specification path, also with per-route middleware

Helper functions are available for all of these to customise each created object.

Interactions with each wrapper will automatically trigger the corresponding changes to the OpenAPI specification, and this can be directly modified as well both before the server is started, or on the fly when serving the spec to clients through the API.

Certain objects in OpenAPI specs can either be a value or a reference to a value elsewhere in the specification. See openapi/v3.1.0/ref.go for the Ref[T any] struct type. This uses generics, requiring Go 1.18+. echOpen is tested against the last three major Go versions.

Features

  • Full access to both the underlying echo engine with support for groups, and the generated OpenAPI schema.
  • Binding of path, query, and request body.
  • Schema generation via reflection for request and response bodies.

Getting Started

// Create a new echOpen wrapper
api := echopen.New(
  "Hello World",
  "1.0.0",
  echopen.WithSpecDescription("Very basic example with single route and plain text response."),
  echopen.WithSpecLicense(&v310.License{Name: "MIT", URL: "https://opensource.org/license/mit/"}),
  echopen.WithSpecTag(&v310.Tag{Name: "hello_world", Description: "Hello World API Routes"}),
)

// Hello World route
api.GET(
  "/hello",
  hello,
  echopen.WithTags("hello_world"),
  echopen.WithResponseStruct(fmt.Sprint(http.StatusOK), "Default response", ""),
  echopen.WithResponseDescription("default", "Unexpected error"),
)

// Serve the generated schema
api.ServeYAMLSpec("/openapi.yml")
api.ServeJSONSpec("/openapi.json")
api.ServeUI("/", "/openapi.yml", "5.10.3")

// Start the server
api.Start("localhost:3000")

This results in the following generated specification:

openapi: 3.1.0
jsonSchemaDialect: https://spec.openapis.org/oas/3.1/dialect/base
info:
  title: Hello World
  version: 1.0.0
  description: Very basic example with single route and plain text response.
  license:
    name: MIT
    url: https://example.com/license
tags:
  - name: hello_world
    description: Hello World API Routes
paths:
  /hello:
    get:
      operationId: getHello
      tags:
        - hello_world
      responses:
        "200":
          description: Default response
          content:
            text/plain:
              schema:
                type: string
        default:
          description: Unexpected error

The call to echopen.New() creates a new wrapper around an echo engine and a v3.1.0 schema object. Whilst both of these can be interacted with directly, the libary contains a range of helper functions to simplify building APIs.

Examples

Several examples are provided which illustrate different usage of echOpen. Each one runs its own server and provides a spec browser to test the API.

Each folder also contains the generated OpenAPI spec in openapi_out.yml.

Routes

Adding routes is almost identical to working with the echo engine directly.

// echo add route
func (e *Echo) Add(method string, path string, handler HandlerFunc, middleware ...MiddlewareFunc) *Route {}

// echOpen equivalent
func (w *APIWrapper) Add(method string, path string, handler echo.HandlerFunc, config ...RouteConfigFunc) *RouteWrapper {}

The main difference is optional middleware arguments are swapped for optional configuration functions, of which one is WithMiddlewares(m ...MiddlewareFunc). This allows for broader configuration options of the route and corresponding specification entries. The returned echo.Route instance can be accessed from the RouteWrapper struct Route field.

Routes or middleware that do not appear in the spec can be added to the echo engine directly. The raw echo engine instance can be accessed from the APIWrapper struct Engine field.

Convenience methods for CONNECT, DELETE, GET, HEAD, OPTIONS, PATCH, POST, PUT, and TRACE follow the same function signature as above, minus the method.

Configuration Functions

  • WithOperationID -Overrides the operationId field ot the OpenAPI Operation object with the given string. By default operationId is set to a sensible value by interpolating the path and method into a unique string.
  • WithDescription - Sets the OpenAPI Operation description field, trimming any leading/trailing whitespace.
  • WithTags - Adds a tag to the OpenAPI Operation object for this route with the given name. This tag must have been registered first using WithSpecTag or it will panic.
  • WithMiddlewares - Passes one or more middleware functions to the underlying echo Add function. See Security for more information.
  • WithSecurityRequirement - Adds an OpenAPI Security Requirement object to the OpenAPI Operation. A Security Scheme of the same name must have been registered or it will panic.
  • WithOptionalSecurity- Adds an empty Security Requirement to the Operation. This allows the route validation middleware to treat all other Security Requirement as optional.

Route Groups

Similar to Routes, adding Groups is meant to closely match working with the echo engine directly.

// echo add group
func (e *Echo) Group(prefix string, m ...MiddlewareFunc) (g *Group) {}

// echOpen equivalent
func (w *APIWrapper) Group(prefix string, config ...GroupConfigFunc) *GroupWrapper {}

Middleware for the route group is added via the WithGroupMiddlewares(m ...MiddlewareFunc) helper. The returned echo.Group instance can be accessed from either the GroupWrapper or RouteWrapper structs Group field.

The same Add function for attaching routes to the group is provided on the GroupWrapper, and convenience methods for CONNECT, DELETE, GET, HEAD, OPTIONS, PATCH, POST, PUT, and TRACE follow the same function signature, minus the method.

Groups can also be created under other groups, as well as from the top level engine.

Configuration Functions

  • WithGroupMiddlewares - Provides a list of middlewares that will be passed to the underlying echo.Group() call.
  • WithGroupTags - Calls WithTags for every route added to the group.
  • WithGroupSecurityRequirement - Calls WithSecurityRequirement for every route added to the group.

Route Parameters

Parameters can be provided via query, header, path or cookies. All of these can be automatically extracted from the request and inserted into the request context, throwing ErrRequiredParameterMissing if the required flag is set and the parameter is not supplied.

Location RouteConfigFunc Echo Context Key
header WithHeaderParameterConfig(*HeaderParameterConfig) header.<name> (string/[]string)*
path WithPathParameterConfig(*PathParameterConfig) path.<name> (string)
cookie WithCookieParameterConfig(*CookieParameterConfig) cookie.<name> (string)

(* Depending on the value of config field AllowMultiple. If false, only the first value is used. )

Each of these parameter functions also has a simplified form of the same name, omitting the Config prefix. This can be used where the simplified form is sufficient, complex cases may need the full config.

In the case of header parameters, the config Name field and corresponding default context key placeholder is converted to the canonical header key.

To specify custom parameters with no automatic binding, use WithParameter.

Query Binding

Query/Form parameters can be bound to a single struct with type conversion, which allows schema extraction via reflection and validation.

Location RouteConfigFunc Echo Context Key
query WithQueryStruct(target interface{}) query
query/body WithFormStruct(target interface{}) form

As this should only be used once per route, multiple structs cannot be bound to the incoming query/body form data. The bound value stored in the context will be a pointer to a struct of the same type as the target argument.

For example:

type QueryStruct struct { ... }

api.GET("/", handler, echopen.WithQueryStruct(QueryStruct{}))

func handler(c *echo.Context) error {
  query := c.Get("query").(*QueryStruct{})
  ...
}

Reflection also supports using description and example struct tags to populate the respective fields in the schema.

Responses

Responses can take almost limitless forms in OpenAPI specs. Several helpers are provided to assist with common cases:

  • WithResponse - Adds a custom response object to the operation directly. Offers complete control but does not infer any properties from structs etc.
  • WithResponseDescription - Adds a response for a given code containing only a description. Common way of documenting just the existence of a code.
  • WithResponseRef - Adds a response ref for a registered named response object under the spec #/components/responses map. Panics if the name does not exist.
  • WithResponseFile - Binary response with a given MIME type.
  • WithResponseStructConfig - MIME types and schema inferred from a provided config struct containing a target value.
  • WithResponseStruct - JSON-only version of WithResponseStructConfig

In all cases code is a string to allow the catch-all default case to be specified, e.g.

echopen.WithResponseDescription("default", "Unexpected error"),

Composition

Struct composition is supported and results in an allOf schema:

type NewPet struct {
	Name string `json:"name,omitempty"`
	Tag  string `json:"tag,omitempty"`
}

type Pet struct {
	ID int64 `json:"id,omitempty"`
	NewPet
}

This results in the following schema components:

components:
  schemas:
    NewPet:
      type: object
      properties:
        name:
          type: string
        tag:
          type: string
    Pet:
      allOf:
        - $ref: "#/components/schemas/NewPet"
        - type: object
          properties:
            id:
              type: integer
              format: int64

These excerpts come from the Petstore example.

Validation

Validation is supported, and assumes usage of github.com/go-playground/validator/v10. The following validation tags are extracted and used to update the generated schema as follows:

  • max/lte - MaxLength (string) / Maximum (number/integer) / MaxItems (array)
  • min/gte - MinLength (string) / Minimum (number/integer) / MinItems (array)
  • lt - ExclusiveMinimum (number/integer)
  • gt - ExclusiveMaximum (number/integer)
  • unique - UniqueItems (array)

Validation is performed on all Parameter structs (query/header/path) and Request Bodies.

Validation is not performed on Responses, as the spec is not used to type constrain the route handler functions, and the potentially wide range of responses (both expected and unexpected "default" cases) makes this infeasible.

Security

Adding Schemes

Specifying Requirements

Component Reuse

By default, any schema generated via reflection from a named struct is registered under the spec #/components/schemas map. This cuts down on duplication, however care must be taken to ensure structs with the same name are identical, as the content is not checked.

Documentation

Index

Constants

This section is empty.

Variables

View Source
var (
	ErrRequiredParameterMissing   = fmt.Errorf("echopen: required parameter missing")
	ErrSecurityRequirementsNotMet = fmt.Errorf("echopen: at least one required security scheme must be provided")
	ErrContentTypeNotSupported    = fmt.Errorf("echopen: request did not match defined content types")
)

Functions

func DefaultErrorHandler

func DefaultErrorHandler(err error, c echo.Context)

Extend the default echo handler to cover errors defined by echopen

func ExtractJSONTags

func ExtractJSONTags(field reflect.StructField) (string, bool)

func ExtractValidationRules

func ExtractValidationRules(field reflect.StructField, schema *v310.Schema)

ExtractValidationRules extracts known rules from the "validate" tag. Assumes use of github.com/go-playground/validator/v10

func PtrTo

func PtrTo[T any](v T) *T

Types

type APIWrapper

type APIWrapper struct {
	Spec   *v310.Specification
	Engine *echo.Echo
	Config *Config
	// contains filtered or unexported fields
}

func New

func New(title string, apiVersion string, config ...WrapperConfigFunc) *APIWrapper

func (*APIWrapper) Add

func (w *APIWrapper) Add(method string, path string, handler echo.HandlerFunc, config ...RouteConfigFunc) *RouteWrapper

Register a new route with the given method and path

func (*APIWrapper) DELETE

func (w *APIWrapper) DELETE(path string, handler echo.HandlerFunc, config ...RouteConfigFunc) *RouteWrapper

func (*APIWrapper) GET

func (w *APIWrapper) GET(path string, handler echo.HandlerFunc, config ...RouteConfigFunc) *RouteWrapper

func (*APIWrapper) Group

func (w *APIWrapper) Group(prefix string, config ...GroupConfigFunc) *GroupWrapper

Create a new group with prefix and optional group-specific configuration

func (*APIWrapper) HEAD

func (w *APIWrapper) HEAD(path string, handler echo.HandlerFunc, config ...RouteConfigFunc) *RouteWrapper

func (*APIWrapper) OPTIONS

func (w *APIWrapper) OPTIONS(path string, handler echo.HandlerFunc, config ...RouteConfigFunc) *RouteWrapper

func (*APIWrapper) PATCH

func (w *APIWrapper) PATCH(path string, handler echo.HandlerFunc, config ...RouteConfigFunc) *RouteWrapper

func (*APIWrapper) POST

func (w *APIWrapper) POST(path string, handler echo.HandlerFunc, config ...RouteConfigFunc) *RouteWrapper

func (*APIWrapper) PUT

func (w *APIWrapper) PUT(path string, handler echo.HandlerFunc, config ...RouteConfigFunc) *RouteWrapper

func (*APIWrapper) ServeJSONSpec

func (w *APIWrapper) ServeJSONSpec(path string, filters ...SpecFilterFunc) *echo.Route

func (*APIWrapper) ServeRapidoc

func (w *APIWrapper) ServeRapidoc(path string, schemaPath string) *echo.Route

func (*APIWrapper) ServeSwaggerUI

func (w *APIWrapper) ServeSwaggerUI(path string, schemaPath string, version string) *echo.Route

func (*APIWrapper) ServeYAMLSpec

func (w *APIWrapper) ServeYAMLSpec(path string, filters ...SpecFilterFunc) *echo.Route

func (*APIWrapper) SetBaseURL

func (a *APIWrapper) SetBaseURL(baseURL string)

func (*APIWrapper) SetErrorHandler

func (a *APIWrapper) SetErrorHandler(h echo.HTTPErrorHandler)

func (*APIWrapper) SetSpecContact

func (a *APIWrapper) SetSpecContact(c *v310.Contact)

func (*APIWrapper) SetSpecDescription

func (a *APIWrapper) SetSpecDescription(desc string)

func (*APIWrapper) SetSpecExternalDocs

func (a *APIWrapper) SetSpecExternalDocs(d *v310.ExternalDocs)

func (*APIWrapper) SetSpecLicense

func (a *APIWrapper) SetSpecLicense(l *v310.License)

func (*APIWrapper) SetTermsOfService

func (a *APIWrapper) SetTermsOfService(tos string)

func (*APIWrapper) Start

func (w *APIWrapper) Start(addr string) error

Start starts an HTTP server

func (*APIWrapper) StructFieldToSchemaRef

func (w *APIWrapper) StructFieldToSchemaRef(f reflect.StructField) *v310.Ref[v310.Schema]

func (*APIWrapper) StructTypeToSchema

func (w *APIWrapper) StructTypeToSchema(target reflect.Type, nameTag string) *v310.Schema

StructTypeToSchema iterates over struct fields to build a schema. Assumes JSON content type.

func (*APIWrapper) TRACE

func (w *APIWrapper) TRACE(path string, handler echo.HandlerFunc, config ...RouteConfigFunc) *RouteWrapper

func (*APIWrapper) ToSchemaRef

func (w *APIWrapper) ToSchemaRef(target interface{}) *v310.Ref[v310.Schema]

ToSchemaRef takes a target value, extracts the type information, and returns a SchemaRef for that type

func (*APIWrapper) TypeToSchema

func (w *APIWrapper) TypeToSchema(typ reflect.Type) *v310.Schema

TypeToSchema looks up the schema type for a given reflected type

func (*APIWrapper) TypeToSchemaRef

func (w *APIWrapper) TypeToSchemaRef(typ reflect.Type) *v310.Ref[v310.Schema]

TypeToSchemaRef takes a reflected type and returns a SchemaRef. Where possible a Ref will be returned instead of a Value. Struct names are assumed to be unique and thus conform to the same schema

func (*APIWrapper) WriteYAMLSpec

func (w *APIWrapper) WriteYAMLSpec(path string) error

type Config

type Config struct {
	BaseURL                  string
	DisableDefaultMiddleware bool
}

type CookieParameterConfig

type CookieParameterConfig struct {
	Name        string
	Description string
	Required    bool
	Schema      *v310.Schema
}

type GroupConfigFunc

type GroupConfigFunc func(*GroupWrapper) *GroupWrapper

func WithGroupMiddlewares

func WithGroupMiddlewares(m ...echo.MiddlewareFunc) GroupConfigFunc

func WithGroupSecurityRequirement

func WithGroupSecurityRequirement(req *v310.SecurityRequirement) GroupConfigFunc

func WithGroupTags

func WithGroupTags(tags ...string) GroupConfigFunc

type GroupWrapper

type GroupWrapper struct {
	API                  *APIWrapper
	GroupWrapper         *GroupWrapper
	Prefix               string
	Middlewares          []echo.MiddlewareFunc
	Tags                 []string
	SecurityRequirements []*v310.SecurityRequirement
	RouterGroup          *echo.Group
}

func (*GroupWrapper) Add

func (g *GroupWrapper) Add(method string, path string, handler echo.HandlerFunc, config ...RouteConfigFunc) *RouteWrapper

Add a route to the group

func (*GroupWrapper) DELETE

func (g *GroupWrapper) DELETE(path string, handler echo.HandlerFunc, config ...RouteConfigFunc) *RouteWrapper

func (*GroupWrapper) GET

func (g *GroupWrapper) GET(path string, handler echo.HandlerFunc, config ...RouteConfigFunc) *RouteWrapper

func (*GroupWrapper) Group

func (g *GroupWrapper) Group(prefix string, config ...GroupConfigFunc) *GroupWrapper

Create a new sub-group with prefix and optional group-specific configuration

func (*GroupWrapper) HEAD

func (g *GroupWrapper) HEAD(path string, handler echo.HandlerFunc, config ...RouteConfigFunc) *RouteWrapper

func (*GroupWrapper) OPTIONS

func (g *GroupWrapper) OPTIONS(path string, handler echo.HandlerFunc, config ...RouteConfigFunc) *RouteWrapper

func (*GroupWrapper) PATCH

func (g *GroupWrapper) PATCH(path string, handler echo.HandlerFunc, config ...RouteConfigFunc) *RouteWrapper

func (*GroupWrapper) POST

func (g *GroupWrapper) POST(path string, handler echo.HandlerFunc, config ...RouteConfigFunc) *RouteWrapper

func (*GroupWrapper) PUT

func (g *GroupWrapper) PUT(path string, handler echo.HandlerFunc, config ...RouteConfigFunc) *RouteWrapper

func (*GroupWrapper) TRACE

func (g *GroupWrapper) TRACE(path string, handler echo.HandlerFunc, config ...RouteConfigFunc) *RouteWrapper

type HeaderParameterConfig

type HeaderParameterConfig struct {
	Name          string
	Description   string
	Required      bool
	Examples      []*v310.Example
	Style         string
	Explode       bool
	Schema        *v310.Schema
	AllowMultiple bool
}

type PathParameterConfig

type PathParameterConfig struct {
	Name        string
	Description string
	Examples    []*v310.Example
	Schema      *v310.Schema
}

type ResponseStructConfig

type ResponseStructConfig struct {
	Description string
	Target      interface{}
	JSON        bool
}

type RouteConfigFunc

type RouteConfigFunc func(*RouteWrapper) *RouteWrapper

func WithCookieParameter

func WithCookieParameter(name string, description string, example interface{}) RouteConfigFunc

func WithCookieParameterConfig

func WithCookieParameterConfig(c *CookieParameterConfig) RouteConfigFunc

func WithDeprecated

func WithDeprecated() RouteConfigFunc

func WithDescription

func WithDescription(desc string) RouteConfigFunc

func WithFormStruct

func WithFormStruct(target interface{}) RouteConfigFunc

WithFormStruct extracts type information from a provided struct to populate the OpenAPI operation parameters. A bound struct of the same type is added to the context under the key "form" during each request Binding will use either request body or query params (GET/DELETE only)

func WithHeaderParameter

func WithHeaderParameter(name string, description string, example interface{}) RouteConfigFunc

func WithHeaderParameterConfig

func WithHeaderParameterConfig(c *HeaderParameterConfig) RouteConfigFunc

func WithMiddlewares

func WithMiddlewares(m ...echo.MiddlewareFunc) RouteConfigFunc

func WithOperationID

func WithOperationID(id string) RouteConfigFunc

func WithOptionalSecurity

func WithOptionalSecurity() RouteConfigFunc

func WithParameter

func WithParameter(param *v310.Parameter) RouteConfigFunc

func WithPathParameter

func WithPathParameter(name string, description string, example interface{}) RouteConfigFunc

func WithPathParameterConfig

func WithPathParameterConfig(c *PathParameterConfig) RouteConfigFunc

func WithQueryStruct

func WithQueryStruct(target interface{}) RouteConfigFunc

WithQueryStruct extracts type information from a provided struct to populate the OpenAPI operation parameters. A bound struct of the same type is added to the context under the key "query" during each request

func WithRequestBody

func WithRequestBody(rb *v310.RequestBody) RouteConfigFunc

func WithRequestBodyRef

func WithRequestBodyRef(name string) RouteConfigFunc

func WithRequestBodySchema

func WithRequestBodySchema(mime string, s *v310.Schema) RouteConfigFunc

func WithRequestBodyStruct

func WithRequestBodyStruct(mime string, description string, target interface{}) RouteConfigFunc

WithRequestBodyStruct extracts type information from a provided struct to populate the OpenAPI requestBody. A bound struct of the same type is added to the context under the key "body" during each request.

func WithResponse

func WithResponse(code string, resp *v310.Response) RouteConfigFunc

func WithResponseDescription

func WithResponseDescription(code string, description string) RouteConfigFunc

func WithResponseFile

func WithResponseFile(code string, description string, mime string) RouteConfigFunc

func WithResponseRef

func WithResponseRef(code string, name string) RouteConfigFunc

func WithResponseStruct

func WithResponseStruct(code string, description string, target interface{}) RouteConfigFunc

func WithResponseStructConfig

func WithResponseStructConfig(code string, config *ResponseStructConfig) RouteConfigFunc

func WithResponseType

func WithResponseType(code string, description string, example interface{}) RouteConfigFunc

func WithSecurityRequirement

func WithSecurityRequirement(name string, scopes []string) RouteConfigFunc

WithSecurityRequirement attaches a requirement to a route that the matching security scheme is fulfilled. Attaches middleware that adds the security scheme value and scopes to the context at security.<name> and security.<name>.scopes

func WithSummary

func WithSummary(sum string) RouteConfigFunc

func WithTags

func WithTags(tags ...string) RouteConfigFunc

type RouteWrapper

type RouteWrapper struct {
	API               *APIWrapper
	Group             *GroupWrapper
	Operation         *v310.Operation
	PathItem          *v310.PathItem
	Handler           echo.HandlerFunc
	Middlewares       []echo.MiddlewareFunc
	Route             *echo.Route
	QuerySchema       *v310.Schema
	FormSchema        *v310.Schema
	RequestBodySchema map[string]*v310.Schema
}

type SpecFilterFunc

type SpecFilterFunc = func(s *v310.Specification) *v310.Specification

func ExcludeTags

func ExcludeTags(tags ...string) SpecFilterFunc

func IncludeTags

func IncludeTags(tags ...string) SpecFilterFunc

type WrapperConfigFunc

type WrapperConfigFunc func(*APIWrapper) *APIWrapper

func WithBaseURL

func WithBaseURL(baseURL string) WrapperConfigFunc

func WithSpecContact

func WithSpecContact(c *v310.Contact) WrapperConfigFunc

func WithSpecDescription

func WithSpecDescription(desc string) WrapperConfigFunc

func WithSpecExternalDocs

func WithSpecExternalDocs(d *v310.ExternalDocs) WrapperConfigFunc

func WithSpecLicense

func WithSpecLicense(l *v310.License) WrapperConfigFunc

func WithSpecServer

func WithSpecServer(s *v310.Server) WrapperConfigFunc

func WithSpecTag

func WithSpecTag(t *v310.Tag) WrapperConfigFunc

func WithSpecTermsOfService

func WithSpecTermsOfService(tos string) WrapperConfigFunc

Directories

Path Synopsis
examples
openapi

Jump to

Keyboard shortcuts

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