mirror of
https://github.com/davidebianchi/gswagger.git
synced 2025-12-23 23:38:43 -05:00
317 lines
8.8 KiB
Go
317 lines
8.8 KiB
Go
package swagger
|
|
|
|
import (
|
|
"errors"
|
|
"fmt"
|
|
"path"
|
|
"regexp"
|
|
"sort"
|
|
"strings"
|
|
|
|
"github.com/getkin/kin-openapi/openapi3"
|
|
"github.com/invopop/jsonschema"
|
|
)
|
|
|
|
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 openapi schema, after validating it
|
|
func (r Router[HandlerFunc, Route]) AddRawRoute(method string, routePath string, handler HandlerFunc, operation Operation) (Route, error) {
|
|
op := operation.Operation
|
|
if op != nil {
|
|
err := operation.Validate(r.context)
|
|
if err != nil {
|
|
return getZero[Route](), err
|
|
}
|
|
} else {
|
|
op = openapi3.NewOperation()
|
|
if op.Responses == nil {
|
|
op.Responses = openapi3.NewResponses()
|
|
}
|
|
}
|
|
pathWithPrefix := path.Join(r.pathPrefix, routePath)
|
|
oasPath := r.router.TransformPathToOasPath(pathWithPrefix)
|
|
r.swaggerSchema.AddOperation(oasPath, method, op)
|
|
|
|
// Handle, when content-type is json, the request/response marshalling? Maybe with a specific option.
|
|
return r.router.AddRoute(method, pathWithPrefix, 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
|
|
}
|
|
|
|
type Parameter struct {
|
|
Content Content
|
|
Schema *Schema
|
|
Description string
|
|
}
|
|
|
|
// ParameterValue is the struct containing the schema or the content information.
|
|
// If content is specified, it takes precedence.
|
|
type ParameterValue map[string]Parameter
|
|
|
|
// ContentValue is the struct containing the content information.
|
|
type ContentValue struct {
|
|
Content Content
|
|
Description string
|
|
}
|
|
|
|
type SecurityRequirements []SecurityRequirement
|
|
type SecurityRequirement map[string][]string
|
|
|
|
// Definitions of the route.
|
|
// To see how to use, refer to https://github.com/OAI/OpenAPI-Specification/blob/main/versions/3.0.3.md
|
|
type Definitions struct {
|
|
// Specification extensions https://github.com/OAI/OpenAPI-Specification/blob/main/versions/3.0.3.md#specification-extensions
|
|
Extensions map[string]interface{}
|
|
// Optional field for documentation
|
|
Tags []string
|
|
Summary string
|
|
Description string
|
|
Deprecated bool
|
|
|
|
// PathParams contains the path parameters. If empty is autocompleted from the path
|
|
PathParams ParameterValue
|
|
Querystring ParameterValue
|
|
Headers ParameterValue
|
|
Cookies ParameterValue
|
|
RequestBody *ContentValue
|
|
Responses map[int]ContentValue
|
|
|
|
Security SecurityRequirements
|
|
}
|
|
|
|
func newOperationFromDefinition(schema Definitions) Operation {
|
|
operation := NewOperation()
|
|
operation.Responses = &openapi3.Responses{}
|
|
operation.Tags = schema.Tags
|
|
operation.Extensions = schema.Extensions
|
|
operation.addSecurityRequirements(schema.Security)
|
|
operation.Description = schema.Description
|
|
operation.Summary = schema.Summary
|
|
operation.Deprecated = schema.Deprecated
|
|
|
|
return operation
|
|
}
|
|
|
|
const (
|
|
pathParamsType = "path"
|
|
queryParamType = "query"
|
|
headerParamType = "header"
|
|
cookieParamType = "cookie"
|
|
)
|
|
|
|
// AddRoute add a route with json schema inferred by passed schema.
|
|
func (r Router[HandlerFunc, Route]) AddRoute(method string, path string, handler HandlerFunc, schema Definitions) (Route, error) {
|
|
operation := newOperationFromDefinition(schema)
|
|
|
|
err := r.resolveRequestBodySchema(schema.RequestBody, operation)
|
|
if err != nil {
|
|
return getZero[Route](), fmt.Errorf("%w: %s", ErrRequestBody, err)
|
|
}
|
|
|
|
err = r.resolveResponsesSchema(schema.Responses, operation)
|
|
if err != nil {
|
|
return getZero[Route](), fmt.Errorf("%w: %s", ErrResponses, err)
|
|
}
|
|
|
|
oasPath := r.router.TransformPathToOasPath(path)
|
|
err = r.resolveParameterSchema(pathParamsType, getPathParamsAutoComplete(schema, oasPath), operation)
|
|
if err != nil {
|
|
return getZero[Route](), fmt.Errorf("%w: %s", ErrPathParams, err)
|
|
}
|
|
|
|
err = r.resolveParameterSchema(queryParamType, schema.Querystring, operation)
|
|
if err != nil {
|
|
return getZero[Route](), fmt.Errorf("%w: %s", ErrPathParams, err)
|
|
}
|
|
|
|
err = r.resolveParameterSchema(headerParamType, schema.Headers, operation)
|
|
if err != nil {
|
|
return getZero[Route](), fmt.Errorf("%w: %s", ErrPathParams, err)
|
|
}
|
|
|
|
err = r.resolveParameterSchema(cookieParamType, schema.Cookies, operation)
|
|
if err != nil {
|
|
return getZero[Route](), 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,
|
|
Anonymous: true,
|
|
}
|
|
|
|
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
|
|
}
|
|
|
|
func getPathParamsAutoComplete(schema Definitions, path string) ParameterValue {
|
|
if schema.PathParams == nil {
|
|
re := regexp.MustCompile(`\{([^}]+)\}`)
|
|
segments := strings.Split(path, "/")
|
|
for _, segment := range segments {
|
|
params := re.FindAllStringSubmatch(segment, -1)
|
|
if len(params) == 0 {
|
|
continue
|
|
}
|
|
if schema.PathParams == nil {
|
|
schema.PathParams = make(ParameterValue)
|
|
}
|
|
for _, param := range params {
|
|
schema.PathParams[param[1]] = Parameter{
|
|
Schema: &Schema{Value: ""},
|
|
}
|
|
}
|
|
}
|
|
}
|
|
return schema.PathParams
|
|
}
|
|
|
|
func getZero[T any]() T {
|
|
var result T
|
|
return result
|
|
}
|