Files
gswagger/route.go
2021-11-09 22:35:17 +01:00

257 lines
6.9 KiB
Go

package swagger
import (
"errors"
"fmt"
"path"
"sort"
"github.com/alecthomas/jsonschema"
"github.com/davidebianchi/gswagger/apirouter"
"github.com/getkin/kin-openapi/openapi3"
)
var (
// ErrResponses is thrown if error occurs generating responses schemas.
ErrResponses = errors.New("errors generating responses schema")
// ErrRequestBody is thrown if error occurs generating responses schemas.
ErrRequestBody = errors.New("errors generating request body schema")
// ErrPathParams is thrown if error occurs generating path params schemas.
ErrPathParams = errors.New("errors generating path parameters schema")
// ErrQuerystring is thrown if error occurs generating querystring params schemas.
ErrQuerystring = errors.New("errors generating querystring schema")
)
// AddRawRoute add route to router with specific method, path and handler. Add the
// router also to the swagger schema, after validating it
func (r Router) AddRawRoute(method string, routePath string, handler apirouter.HandlerFunc, operation Operation) (interface{}, error) {
op := operation.Operation
if op != nil {
err := operation.Validate(r.context)
if err != nil {
return nil, err
}
} else {
op = openapi3.NewOperation()
if op.Responses == nil {
op.Responses = openapi3.NewResponses()
}
}
pathWithPrefix := path.Join(r.pathPrefix, routePath)
r.swaggerSchema.AddOperation(pathWithPrefix, method, op)
// Handle, when content-type is json, the request/response marshalling? Maybe with a specific option.
return r.router.AddRoute(pathWithPrefix, method, handler), nil
}
// Content is the type of a content.
// The key of the map define the content-type.
type Content map[string]Schema
// Schema contains the value and if properties allow additional properties.
type Schema struct {
Value interface{}
AllowAdditionalProperties bool
}
// ParameterValue is the struct containing the schema or the content information.
// If content is specified, it takes precedence.
type ParameterValue map[string]struct {
Content Content
Schema *Schema
Description string
}
// ContentValue is the struct containing the content information.
type ContentValue struct {
Content Content
Description string
}
// Definitions of the route.
type Definitions struct {
PathParams ParameterValue
Querystring ParameterValue
Headers ParameterValue
Cookies ParameterValue
RequestBody *ContentValue
Responses map[int]ContentValue
}
const (
pathParamsType = "path"
queryParamType = "query"
headerParamType = "header"
cookieParamType = "cookie"
)
// AddRoute add a route with json schema inferted by passed schema.
func (r Router) AddRoute(method string, path string, handler apirouter.HandlerFunc, schema Definitions) (interface{}, error) {
operation := NewOperation()
operation.Responses = make(openapi3.Responses)
err := r.resolveRequestBodySchema(schema.RequestBody, operation)
if err != nil {
return nil, fmt.Errorf("%w: %s", ErrRequestBody, err)
}
err = r.resolveResponsesSchema(schema.Responses, operation)
if err != nil {
return nil, fmt.Errorf("%w: %s", ErrResponses, err)
}
err = r.resolveParameterSchema(pathParamsType, schema.PathParams, operation)
if err != nil {
return nil, fmt.Errorf("%w: %s", ErrPathParams, err)
}
err = r.resolveParameterSchema(queryParamType, schema.Querystring, operation)
if err != nil {
return nil, fmt.Errorf("%w: %s", ErrPathParams, err)
}
err = r.resolveParameterSchema(headerParamType, schema.Headers, operation)
if err != nil {
return nil, fmt.Errorf("%w: %s", ErrPathParams, err)
}
err = r.resolveParameterSchema(cookieParamType, schema.Cookies, operation)
if err != nil {
return nil, fmt.Errorf("%w: %s", ErrPathParams, err)
}
return r.AddRawRoute(method, path, handler, operation)
}
func (r Router) getSchemaFromInterface(v interface{}, allowAdditionalProperties bool) (*openapi3.Schema, error) {
if v == nil {
return &openapi3.Schema{}, nil
}
reflector := &jsonschema.Reflector{
DoNotReference: true,
AllowAdditionalProperties: allowAdditionalProperties,
}
jsonSchema := reflector.Reflect(v)
jsonschema.Version = ""
// Empty definitions. Definitions are not valid in openapi3, which use components.
// In the future, we could add an option to fill the components in openapi spec.
jsonSchema.Definitions = nil
data, err := jsonSchema.MarshalJSON()
if err != nil {
return nil, err
}
schema := openapi3.NewSchema()
err = schema.UnmarshalJSON(data)
if err != nil {
return nil, err
}
return schema, nil
}
func (r Router) resolveRequestBodySchema(bodySchema *ContentValue, operation Operation) error {
if bodySchema == nil {
return nil
}
content, err := r.addContentToOASSchema(bodySchema.Content)
if err != nil {
return err
}
requestBody := openapi3.NewRequestBody().WithContent(content)
if bodySchema.Description != "" {
requestBody.WithDescription(bodySchema.Description)
}
operation.AddRequestBody(requestBody)
return nil
}
func (r Router) resolveResponsesSchema(responses map[int]ContentValue, operation Operation) error {
if responses == nil {
operation.Responses = openapi3.NewResponses()
}
for statusCode, v := range responses {
response := openapi3.NewResponse()
content, err := r.addContentToOASSchema(v.Content)
if err != nil {
return err
}
response = response.WithContent(content)
response = response.WithDescription(v.Description)
operation.AddResponse(statusCode, response)
}
return nil
}
func (r Router) resolveParameterSchema(paramType string, paramConfig ParameterValue, operation Operation) error {
var keys = make([]string, 0, len(paramConfig))
for k := range paramConfig {
keys = append(keys, k)
}
sort.Strings(keys)
for _, key := range keys {
v := paramConfig[key]
var param *openapi3.Parameter
switch paramType {
case pathParamsType:
param = openapi3.NewPathParameter(key)
case queryParamType:
param = openapi3.NewQueryParameter(key)
case headerParamType:
param = openapi3.NewHeaderParameter(key)
case cookieParamType:
param = openapi3.NewCookieParameter(key)
default:
return fmt.Errorf("invalid param type")
}
if v.Description != "" {
param = param.WithDescription(v.Description)
}
if v.Content != nil {
content, err := r.addContentToOASSchema(v.Content)
if err != nil {
return err
}
param.Content = content
} else {
schema := openapi3.NewSchema()
if v.Schema != nil {
var err error
schema, err = r.getSchemaFromInterface(v.Schema.Value, v.Schema.AllowAdditionalProperties)
if err != nil {
return err
}
}
param.WithSchema(schema)
}
operation.AddParameter(param)
}
return nil
}
func (r Router) addContentToOASSchema(content Content) (openapi3.Content, error) {
oasContent := openapi3.NewContent()
for k, v := range content {
var err error
schema, err := r.getSchemaFromInterface(v.Value, v.AllowAdditionalProperties)
if err != nil {
return nil, err
}
oasContent[k] = openapi3.NewMediaType().WithSchema(schema)
}
return oasContent, nil
}