gitea/vendor/github.com/go-swagger/go-swagger/generator/operation.go

1151 lines
39 KiB
Go

// Copyright 2015 go-swagger maintainers
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package generator
import (
"encoding/json"
"errors"
"fmt"
"os"
"path"
"path/filepath"
"sort"
"strings"
"github.com/go-openapi/analysis"
"github.com/go-openapi/loads"
"github.com/go-openapi/runtime"
"github.com/go-openapi/spec"
"github.com/go-openapi/swag"
)
type respSort struct {
Code int
Response spec.Response
}
type responses []respSort
func (s responses) Len() int { return len(s) }
func (s responses) Swap(i, j int) { s[i], s[j] = s[j], s[i] }
func (s responses) Less(i, j int) bool { return s[i].Code < s[j].Code }
// sortedResponses produces a sorted list of responses.
// TODO: this is redundant with the definition given in struct.go
func sortedResponses(input map[int]spec.Response) responses {
var res responses
for k, v := range input {
if k > 0 {
res = append(res, respSort{k, v})
}
}
sort.Sort(res)
return res
}
// GenerateServerOperation generates a parameter model, parameter validator, http handler implementations for a given operation
// It also generates an operation handler interface that uses the parameter model for handling a valid request.
// Allows for specifying a list of tags to include only certain tags for the generation
func GenerateServerOperation(operationNames []string, opts *GenOpts) error {
if opts == nil {
return errors.New("gen opts are required")
}
templates.LoadDefaults()
templates.SetAllowOverride(opts.AllowTemplateOverride)
if opts.TemplateDir != "" {
if err := templates.LoadDir(opts.TemplateDir); err != nil {
return err
}
}
if err := opts.CheckOpts(); err != nil {
return err
}
// Load the spec
_, specDoc, err := loadSpec(opts.Spec)
if err != nil {
return err
}
// Validate and Expand. specDoc is in/out param.
specDoc, err = validateAndFlattenSpec(opts, specDoc)
if err != nil {
return err
}
analyzed := analysis.New(specDoc.Spec())
ops := gatherOperations(analyzed, operationNames)
if len(ops) == 0 {
return errors.New("no operations were selected")
}
for operationName, opRef := range ops {
method, path, operation := opRef.Method, opRef.Path, opRef.Op
defaultScheme := opts.DefaultScheme
if defaultScheme == "" {
defaultScheme = sHTTP
}
defaultProduces := opts.DefaultProduces
if defaultProduces == "" {
defaultProduces = runtime.JSONMime
}
defaultConsumes := opts.DefaultConsumes
if defaultConsumes == "" {
defaultConsumes = runtime.JSONMime
}
serverPackage := opts.LanguageOpts.ManglePackagePath(opts.ServerPackage, "server")
generator := operationGenerator{
Name: operationName,
Method: method,
Path: path,
BasePath: specDoc.BasePath(),
APIPackage: opts.LanguageOpts.ManglePackagePath(opts.APIPackage, "api"),
ModelsPackage: opts.LanguageOpts.ManglePackagePath(opts.ModelPackage, "definitions"),
ClientPackage: opts.LanguageOpts.ManglePackagePath(opts.ClientPackage, "client"),
ServerPackage: serverPackage,
Operation: *operation,
SecurityRequirements: analyzed.SecurityRequirementsFor(operation),
SecurityDefinitions: analyzed.SecurityDefinitionsFor(operation),
Principal: opts.Principal,
Target: filepath.Join(opts.Target, filepath.FromSlash(serverPackage)),
Base: opts.Target,
Tags: opts.Tags,
IncludeHandler: opts.IncludeHandler,
IncludeParameters: opts.IncludeParameters,
IncludeResponses: opts.IncludeResponses,
IncludeValidator: true, // we no more support the CLI option to disable validation
DumpData: opts.DumpData,
DefaultScheme: defaultScheme,
DefaultProduces: defaultProduces,
DefaultConsumes: defaultConsumes,
Doc: specDoc,
Analyzed: analyzed,
GenOpts: opts,
}
if err := generator.Generate(); err != nil {
return err
}
}
return nil
}
type operationGenerator struct {
Authorized bool
IncludeHandler bool
IncludeParameters bool
IncludeResponses bool
IncludeValidator bool
DumpData bool
Principal string
Target string
Base string
Name string
Method string
Path string
BasePath string
APIPackage string
ModelsPackage string
ServerPackage string
ClientPackage string
Operation spec.Operation
SecurityRequirements [][]analysis.SecurityRequirement
SecurityDefinitions map[string]spec.SecurityScheme
Tags []string
DefaultScheme string
DefaultProduces string
DefaultConsumes string
Doc *loads.Document
Analyzed *analysis.Spec
GenOpts *GenOpts
}
func intersectTags(left, right []string) (filtered []string) {
if len(right) == 0 {
filtered = left
return
}
for _, l := range left {
if containsString(right, l) {
filtered = append(filtered, l)
}
}
return
}
func (o *operationGenerator) Generate() error {
// Build a list of codegen operations based on the tags,
// the tag decides the actual package for an operation
// the user specified package serves as root for generating the directory structure
var operations GenOperations
authed := len(o.SecurityRequirements) > 0
var bldr codeGenOpBuilder
bldr.Name = o.Name
bldr.Method = o.Method
bldr.Path = o.Path
bldr.BasePath = o.BasePath
bldr.ModelsPackage = o.ModelsPackage
bldr.Principal = o.Principal
bldr.Target = o.Target
bldr.Operation = o.Operation
bldr.Authed = authed
bldr.Security = o.SecurityRequirements
bldr.SecurityDefinitions = o.SecurityDefinitions
bldr.Doc = o.Doc
bldr.Analyzed = o.Analyzed
bldr.DefaultScheme = o.DefaultScheme
bldr.DefaultProduces = o.DefaultProduces
bldr.RootAPIPackage = o.GenOpts.LanguageOpts.ManglePackageName(o.ServerPackage, "server")
bldr.GenOpts = o.GenOpts
bldr.DefaultConsumes = o.DefaultConsumes
bldr.IncludeValidator = o.IncludeValidator
bldr.DefaultImports = []string{o.GenOpts.ExistingModels}
if o.GenOpts.ExistingModels == "" {
bldr.DefaultImports = []string{
path.Join(
filepath.ToSlash(o.GenOpts.LanguageOpts.baseImport(o.Base)),
o.GenOpts.LanguageOpts.ManglePackagePath(o.ModelsPackage, "")),
}
}
bldr.APIPackage = o.APIPackage
st := o.Tags
if o.GenOpts != nil {
st = o.GenOpts.Tags
}
intersected := intersectTags(o.Operation.Tags, st)
if len(intersected) > 0 {
tag := intersected[0]
bldr.APIPackage = o.GenOpts.LanguageOpts.ManglePackagePath(tag, o.APIPackage)
}
op, err := bldr.MakeOperation()
if err != nil {
return err
}
op.Tags = intersected
operations = append(operations, op)
sort.Sort(operations)
for _, op := range operations {
if o.GenOpts.DumpData {
bb, _ := json.MarshalIndent(swag.ToDynamicJSON(op), "", " ")
fmt.Fprintln(os.Stdout, string(bb))
continue
}
if err := o.GenOpts.renderOperation(&op); err != nil {
return err
}
}
return nil
}
type codeGenOpBuilder struct {
Authed bool
IncludeValidator bool
Name string
Method string
Path string
BasePath string
APIPackage string
RootAPIPackage string
ModelsPackage string
Principal string
Target string
Operation spec.Operation
Doc *loads.Document
Analyzed *analysis.Spec
DefaultImports []string
Imports map[string]string
DefaultScheme string
DefaultProduces string
DefaultConsumes string
Security [][]analysis.SecurityRequirement
SecurityDefinitions map[string]spec.SecurityScheme
ExtraSchemas map[string]GenSchema
GenOpts *GenOpts
}
// renameTimeout renames the variable in use by client template to avoid conflicting
// with param names.
func renameTimeout(seenIds map[string][]string, current string) string {
var next string
switch strings.ToLower(current) {
case "timeout":
next = "requestTimeout"
case "requesttimeout":
next = "httpRequestTimeout"
case "httptrequesttimeout":
next = "swaggerTimeout"
case "swaggertimeout":
next = "operationTimeout"
case "operationtimeout":
next = "opTimeout"
case "optimeout":
next = "operTimeout"
}
if _, ok := seenIds[next]; ok {
return renameTimeout(seenIds, next)
}
return next
}
func (b *codeGenOpBuilder) MakeOperation() (GenOperation, error) {
debugLog("[%s %s] parsing operation (id: %q)", b.Method, b.Path, b.Operation.ID)
// NOTE: we assume flatten is enabled by default (i.e. complex constructs are resolved from the models package),
// but do not assume the spec is necessarily fully flattened (i.e. all schemas moved to definitions).
//
// Fully flattened means that all complex constructs are present as
// definitions and models produced accordingly in ModelsPackage,
// whereas minimal flatten simply ensures that there are no weird $ref's in the spec.
//
// When some complex anonymous constructs are specified, extra schemas are produced in the operations package.
//
// In all cases, resetting definitions to the _original_ (untransformed) spec is not an option:
// we take from there the spec possibly already transformed by the GenDefinitions stage.
resolver := newTypeResolver(b.GenOpts.LanguageOpts.ManglePackageName(b.ModelsPackage, "models"), b.Doc)
receiver := "o"
operation := b.Operation
var params, qp, pp, hp, fp GenParameters
var hasQueryParams, hasPathParams, hasHeaderParams, hasFormParams, hasFileParams, hasFormValueParams, hasBodyParams bool
paramsForOperation := b.Analyzed.ParamsFor(b.Method, b.Path)
timeoutName := "timeout"
idMapping := map[string]map[string]string{
"query": make(map[string]string, len(paramsForOperation)),
"path": make(map[string]string, len(paramsForOperation)),
"formData": make(map[string]string, len(paramsForOperation)),
"header": make(map[string]string, len(paramsForOperation)),
"body": make(map[string]string, len(paramsForOperation)),
}
seenIds := make(map[string][]string, len(paramsForOperation))
for id, p := range paramsForOperation {
if _, ok := seenIds[p.Name]; ok {
idMapping[p.In][p.Name] = swag.ToGoName(id)
} else {
idMapping[p.In][p.Name] = swag.ToGoName(p.Name)
}
seenIds[p.Name] = append(seenIds[p.Name], p.In)
if strings.EqualFold(p.Name, timeoutName) {
timeoutName = renameTimeout(seenIds, timeoutName)
}
}
for _, p := range paramsForOperation {
cp, err := b.MakeParameter(receiver, resolver, p, idMapping)
if err != nil {
return GenOperation{}, err
}
if cp.IsQueryParam() {
hasQueryParams = true
qp = append(qp, cp)
}
if cp.IsFormParam() {
if p.Type == file {
hasFileParams = true
}
if p.Type != file {
hasFormValueParams = true
}
hasFormParams = true
fp = append(fp, cp)
}
if cp.IsPathParam() {
hasPathParams = true
pp = append(pp, cp)
}
if cp.IsHeaderParam() {
hasHeaderParams = true
hp = append(hp, cp)
}
if cp.IsBodyParam() {
hasBodyParams = true
}
params = append(params, cp)
}
sort.Sort(params)
sort.Sort(qp)
sort.Sort(pp)
sort.Sort(hp)
sort.Sort(fp)
var srs responses
if operation.Responses != nil {
srs = sortedResponses(operation.Responses.StatusCodeResponses)
}
responses := make([]GenResponse, 0, len(srs))
var defaultResponse *GenResponse
var successResponses []GenResponse
if operation.Responses != nil {
for _, v := range srs {
name, ok := v.Response.Extensions.GetString(xGoName)
if !ok {
// look for name of well-known codes
name = runtime.Statuses[v.Code]
if name == "" {
// non-standard codes deserve some name
name = fmt.Sprintf("Status %d", v.Code)
}
}
name = swag.ToJSONName(b.Name + " " + name)
isSuccess := v.Code/100 == 2
gr, err := b.MakeResponse(receiver, name, isSuccess, resolver, v.Code, v.Response)
if err != nil {
return GenOperation{}, err
}
if isSuccess {
successResponses = append(successResponses, gr)
}
responses = append(responses, gr)
}
if operation.Responses.Default != nil {
gr, err := b.MakeResponse(receiver, b.Name+" default", false, resolver, -1, *operation.Responses.Default)
if err != nil {
return GenOperation{}, err
}
defaultResponse = &gr
}
}
// Always render a default response, even when no responses were defined
if operation.Responses == nil || (operation.Responses.Default == nil && len(srs) == 0) {
gr, err := b.MakeResponse(receiver, b.Name+" default", false, resolver, -1, spec.Response{})
if err != nil {
return GenOperation{}, err
}
defaultResponse = &gr
}
if b.Principal == "" {
b.Principal = iface
}
swsp := resolver.Doc.Spec()
var extraSchemes []string
if ess, ok := operation.Extensions.GetStringSlice(xSchemes); ok {
extraSchemes = append(extraSchemes, ess...)
}
if ess1, ok := swsp.Extensions.GetStringSlice(xSchemes); ok {
extraSchemes = concatUnique(ess1, extraSchemes)
}
sort.Strings(extraSchemes)
schemes := concatUnique(swsp.Schemes, operation.Schemes)
sort.Strings(schemes)
produces := producesOrDefault(operation.Produces, swsp.Produces, b.DefaultProduces)
sort.Strings(produces)
consumes := producesOrDefault(operation.Consumes, swsp.Consumes, b.DefaultConsumes)
sort.Strings(consumes)
var hasStreamingResponse bool
if defaultResponse != nil && defaultResponse.Schema != nil && defaultResponse.Schema.IsStream {
hasStreamingResponse = true
}
var successResponse *GenResponse
for _, sr := range successResponses {
if sr.IsSuccess {
successResponse = &sr
break
}
}
for _, sr := range successResponses {
if !hasStreamingResponse && sr.Schema != nil && sr.Schema.IsStream {
hasStreamingResponse = true
break
}
}
if !hasStreamingResponse {
for _, r := range responses {
if r.Schema != nil && r.Schema.IsStream {
hasStreamingResponse = true
break
}
}
}
return GenOperation{
GenCommon: GenCommon{
Copyright: b.GenOpts.Copyright,
TargetImportPath: filepath.ToSlash(b.GenOpts.LanguageOpts.baseImport(b.GenOpts.Target)),
},
Package: b.GenOpts.LanguageOpts.ManglePackageName(b.APIPackage, "api"),
RootPackage: b.RootAPIPackage,
Name: b.Name,
Method: b.Method,
Path: b.Path,
BasePath: b.BasePath,
Tags: operation.Tags,
Description: trimBOM(operation.Description),
ReceiverName: receiver,
DefaultImports: b.DefaultImports,
Imports: b.Imports,
Params: params,
Summary: trimBOM(operation.Summary),
QueryParams: qp,
PathParams: pp,
HeaderParams: hp,
FormParams: fp,
HasQueryParams: hasQueryParams,
HasPathParams: hasPathParams,
HasHeaderParams: hasHeaderParams,
HasFormParams: hasFormParams,
HasFormValueParams: hasFormValueParams,
HasFileParams: hasFileParams,
HasBodyParams: hasBodyParams,
HasStreamingResponse: hasStreamingResponse,
Authorized: b.Authed,
Security: b.makeSecurityRequirements(receiver),
SecurityDefinitions: b.makeSecuritySchemes(receiver),
Principal: b.Principal,
Responses: responses,
DefaultResponse: defaultResponse,
SuccessResponse: successResponse,
SuccessResponses: successResponses,
ExtraSchemas: gatherExtraSchemas(b.ExtraSchemas),
Schemes: schemeOrDefault(schemes, b.DefaultScheme),
ProducesMediaTypes: produces,
ConsumesMediaTypes: consumes,
ExtraSchemes: extraSchemes,
TimeoutName: timeoutName,
Extensions: operation.Extensions,
}, nil
}
func producesOrDefault(produces []string, fallback []string, defaultProduces string) []string {
if len(produces) > 0 {
return produces
}
if len(fallback) > 0 {
return fallback
}
return []string{defaultProduces}
}
func schemeOrDefault(schemes []string, defaultScheme string) []string {
if len(schemes) == 0 {
return []string{defaultScheme}
}
return schemes
}
func concatUnique(collections ...[]string) []string {
resultSet := make(map[string]struct{})
for _, c := range collections {
for _, i := range c {
if _, ok := resultSet[i]; !ok {
resultSet[i] = struct{}{}
}
}
}
var result []string
for k := range resultSet {
result = append(result, k)
}
return result
}
func (b *codeGenOpBuilder) MakeResponse(receiver, name string, isSuccess bool, resolver *typeResolver, code int, resp spec.Response) (GenResponse, error) {
debugLog("[%s %s] making id %q", b.Method, b.Path, b.Operation.ID)
// assume minimal flattening has been carried on, so there is not $ref in response (but some may remain in response schema)
res := GenResponse{
Package: b.GenOpts.LanguageOpts.ManglePackageName(b.APIPackage, "api"),
ModelsPackage: b.ModelsPackage,
ReceiverName: receiver,
Name: name,
Description: trimBOM(resp.Description),
DefaultImports: b.DefaultImports,
Imports: b.Imports,
IsSuccess: isSuccess,
Code: code,
Method: b.Method,
Path: b.Path,
Extensions: resp.Extensions,
}
// prepare response headers
for hName, header := range resp.Headers {
hdr, err := b.MakeHeader(receiver, hName, header)
if err != nil {
return GenResponse{}, err
}
res.Headers = append(res.Headers, hdr)
}
sort.Sort(res.Headers)
if resp.Schema != nil {
// resolve schema model
schema, ers := b.buildOperationSchema(fmt.Sprintf("%q", name), name+"Body", swag.ToGoName(name+"Body"), receiver, "i", resp.Schema, resolver)
if ers != nil {
return GenResponse{}, ers
}
res.Schema = &schema
}
return res, nil
}
func (b *codeGenOpBuilder) MakeHeader(receiver, name string, hdr spec.Header) (GenHeader, error) {
tpe := typeForHeader(hdr) //simpleResolvedType(hdr.Type, hdr.Format, hdr.Items)
id := swag.ToGoName(name)
res := GenHeader{
sharedValidations: sharedValidationsFromSimple(hdr.CommonValidations, true), // NOTE: Required is not defined by the Swagger schema for header. Set arbitrarily to true for convenience in templates.
resolvedType: tpe,
Package: b.GenOpts.LanguageOpts.ManglePackageName(b.APIPackage, "api"),
ReceiverName: receiver,
ID: id,
Name: name,
Path: fmt.Sprintf("%q", name),
ValueExpression: fmt.Sprintf("%s.%s", receiver, id),
Description: trimBOM(hdr.Description),
Default: hdr.Default,
HasDefault: hdr.Default != nil,
Converter: stringConverters[tpe.GoType],
Formatter: stringFormatters[tpe.GoType],
ZeroValue: tpe.Zero(),
CollectionFormat: hdr.CollectionFormat,
IndexVar: "i",
}
res.HasValidations, res.HasSliceValidations = b.HasValidations(hdr.CommonValidations, res.resolvedType)
hasChildValidations := false
if hdr.Items != nil {
pi, err := b.MakeHeaderItem(receiver, name+" "+res.IndexVar, res.IndexVar+"i", "fmt.Sprintf(\"%s.%v\", \"header\", "+res.IndexVar+")", res.Name+"I", hdr.Items, nil)
if err != nil {
return GenHeader{}, err
}
res.Child = &pi
hasChildValidations = pi.HasValidations
}
// we feed the GenHeader structure the same way as we do for
// GenParameter, even though there is currently no actual validation
// for response headers.
res.HasValidations = res.HasValidations || hasChildValidations
return res, nil
}
func (b *codeGenOpBuilder) MakeHeaderItem(receiver, paramName, indexVar, path, valueExpression string, items, parent *spec.Items) (GenItems, error) {
var res GenItems
res.resolvedType = simpleResolvedType(items.Type, items.Format, items.Items)
res.sharedValidations = sharedValidationsFromSimple(items.CommonValidations, false)
res.Name = paramName
res.Path = path
res.Location = "header"
res.ValueExpression = swag.ToVarName(valueExpression)
res.CollectionFormat = items.CollectionFormat
res.Converter = stringConverters[res.GoType]
res.Formatter = stringFormatters[res.GoType]
res.IndexVar = indexVar
res.HasValidations, res.HasSliceValidations = b.HasValidations(items.CommonValidations, res.resolvedType)
if items.Items != nil {
// Recursively follows nested arrays
// IMPORTANT! transmitting a ValueExpression consistent with the parent's one
hi, err := b.MakeHeaderItem(receiver, paramName+" "+indexVar, indexVar+"i", "fmt.Sprintf(\"%s.%v\", \"header\", "+indexVar+")", res.ValueExpression+"I", items.Items, items)
if err != nil {
return GenItems{}, err
}
res.Child = &hi
hi.Parent = &res
// Propagates HasValidations flag to outer Items definition (currently not in use: done to remain consistent with parameters)
res.HasValidations = res.HasValidations || hi.HasValidations
}
return res, nil
}
// HasValidations resolves the validation status for simple schema objects
func (b *codeGenOpBuilder) HasValidations(sh spec.CommonValidations, rt resolvedType) (hasValidations bool, hasSliceValidations bool) {
hasNumberValidation := sh.Maximum != nil || sh.Minimum != nil || sh.MultipleOf != nil
hasStringValidation := sh.MaxLength != nil || sh.MinLength != nil || sh.Pattern != ""
hasSliceValidations = sh.MaxItems != nil || sh.MinItems != nil || sh.UniqueItems || len(sh.Enum) > 0
hasValidations = (hasNumberValidation || hasStringValidation || hasSliceValidations || rt.IsCustomFormatter) && !rt.IsStream && !rt.IsInterface
return
}
func (b *codeGenOpBuilder) MakeParameterItem(receiver, paramName, indexVar, path, valueExpression, location string, resolver *typeResolver, items, parent *spec.Items) (GenItems, error) {
debugLog("making parameter item recv=%s param=%s index=%s valueExpr=%s path=%s location=%s", receiver, paramName, indexVar, valueExpression, path, location)
var res GenItems
res.resolvedType = simpleResolvedType(items.Type, items.Format, items.Items)
res.sharedValidations = sharedValidationsFromSimple(items.CommonValidations, false)
res.Name = paramName
res.Path = path
res.Location = location
res.ValueExpression = swag.ToVarName(valueExpression)
res.CollectionFormat = items.CollectionFormat
res.Converter = stringConverters[res.GoType]
res.Formatter = stringFormatters[res.GoType]
res.IndexVar = indexVar
res.HasValidations, res.HasSliceValidations = b.HasValidations(items.CommonValidations, res.resolvedType)
if items.Items != nil {
// Recursively follows nested arrays
// IMPORTANT! transmitting a ValueExpression consistent with the parent's one
pi, err := b.MakeParameterItem(receiver, paramName+" "+indexVar, indexVar+"i", "fmt.Sprintf(\"%s.%v\", "+path+", "+indexVar+")", res.ValueExpression+"I", location, resolver, items.Items, items)
if err != nil {
return GenItems{}, err
}
res.Child = &pi
pi.Parent = &res
// Propagates HasValidations flag to outer Items definition
res.HasValidations = res.HasValidations || pi.HasValidations
}
return res, nil
}
func (b *codeGenOpBuilder) MakeParameter(receiver string, resolver *typeResolver, param spec.Parameter, idMapping map[string]map[string]string) (GenParameter, error) {
debugLog("[%s %s] making parameter %q", b.Method, b.Path, param.Name)
// assume minimal flattening has been carried on, so there is not $ref in response (but some may remain in response schema)
var child *GenItems
id := swag.ToGoName(param.Name)
if len(idMapping) > 0 {
id = idMapping[param.In][param.Name]
}
res := GenParameter{
ID: id,
Name: param.Name,
ModelsPackage: b.ModelsPackage,
Path: fmt.Sprintf("%q", param.Name),
ValueExpression: fmt.Sprintf("%s.%s", receiver, id),
IndexVar: "i",
Default: param.Default,
HasDefault: param.Default != nil,
Description: trimBOM(param.Description),
ReceiverName: receiver,
CollectionFormat: param.CollectionFormat,
Child: child,
Location: param.In,
AllowEmptyValue: (param.In == "query" || param.In == "formData") && param.AllowEmptyValue,
Extensions: param.Extensions,
}
if param.In == "body" {
// Process parameters declared in body (i.e. have a Schema)
res.Required = param.Required
if err := b.MakeBodyParameter(&res, resolver, param.Schema); err != nil {
return GenParameter{}, err
}
} else {
// Process parameters declared in other inputs: path, query, header (SimpleSchema)
res.resolvedType = simpleResolvedType(param.Type, param.Format, param.Items)
res.sharedValidations = sharedValidationsFromSimple(param.CommonValidations, param.Required)
res.ZeroValue = res.resolvedType.Zero()
hasChildValidations := false
if param.Items != nil {
// Follow Items definition for array parameters
pi, err := b.MakeParameterItem(receiver, param.Name+" "+res.IndexVar, res.IndexVar+"i", "fmt.Sprintf(\"%s.%v\", "+res.Path+", "+res.IndexVar+")", res.Name+"I", param.In, resolver, param.Items, nil)
if err != nil {
return GenParameter{}, err
}
res.Child = &pi
// Propagates HasValidations from from child array
hasChildValidations = pi.HasValidations
}
res.IsNullable = !param.Required && !param.AllowEmptyValue
res.HasValidations, res.HasSliceValidations = b.HasValidations(param.CommonValidations, res.resolvedType)
res.HasValidations = res.HasValidations || hasChildValidations
}
// Select codegen strategy for body param validation
res.Converter = stringConverters[res.GoType]
res.Formatter = stringFormatters[res.GoType]
b.setBodyParamValidation(&res)
return res, nil
}
// MakeBodyParameter constructs a body parameter schema
func (b *codeGenOpBuilder) MakeBodyParameter(res *GenParameter, resolver *typeResolver, sch *spec.Schema) error {
// resolve schema model
schema, ers := b.buildOperationSchema(res.Path, b.Operation.ID+"ParamsBody", swag.ToGoName(b.Operation.ID+" Body"), res.ReceiverName, res.IndexVar, sch, resolver)
if ers != nil {
return ers
}
res.Schema = &schema
res.Schema.Required = res.Required // Required in body is managed independently from validations
// build Child items for nested slices and maps
var items *GenItems
res.KeyVar = "k"
res.Schema.KeyVar = "k"
switch {
case schema.IsMap && !schema.IsInterface:
items = b.MakeBodyParameterItemsAndMaps(res, res.Schema.AdditionalProperties)
case schema.IsArray:
items = b.MakeBodyParameterItemsAndMaps(res, res.Schema.Items)
default:
items = new(GenItems)
}
// templates assume at least one .Child != nil
res.Child = items
schema.HasValidations = schema.HasValidations || items.HasValidations
res.resolvedType = schema.resolvedType
// simple and schema views share the same validations
res.sharedValidations = schema.sharedValidations
res.ZeroValue = schema.Zero()
return nil
}
// MakeBodyParameterItemsAndMaps clones the .Items schema structure (resp. .AdditionalProperties) as a .GenItems structure
// for compatibility with simple param templates.
//
// Constructed children assume simple structures: any complex object is assumed to be resolved by a model or extra schema definition
func (b *codeGenOpBuilder) MakeBodyParameterItemsAndMaps(res *GenParameter, it *GenSchema) *GenItems {
items := new(GenItems)
if it != nil {
var prev *GenItems
next := items
if res.Schema.IsArray {
next.Path = "fmt.Sprintf(\"%s.%v\", " + res.Path + ", " + res.IndexVar + ")"
} else if res.Schema.IsMap {
next.Path = "fmt.Sprintf(\"%s.%v\", " + res.Path + ", " + res.KeyVar + ")"
}
next.Name = res.Name + " " + res.Schema.IndexVar
next.IndexVar = res.Schema.IndexVar + "i"
next.KeyVar = res.Schema.KeyVar + "k"
next.ValueExpression = swag.ToVarName(res.Name + "I")
next.Location = "body"
for it != nil {
next.resolvedType = it.resolvedType
next.sharedValidations = it.sharedValidations
next.Formatter = stringFormatters[it.SwaggerFormat]
next.Converter = stringConverters[res.GoType]
next.Parent = prev
_, next.IsCustomFormatter = customFormatters[it.GoType]
next.IsCustomFormatter = next.IsCustomFormatter && !it.IsStream
// special instruction to avoid using CollectionFormat for body params
next.SkipParse = true
if prev != nil {
if prev.IsArray {
next.Path = "fmt.Sprintf(\"%s.%v\", " + prev.Path + ", " + prev.IndexVar + ")"
} else if prev.IsMap {
next.Path = "fmt.Sprintf(\"%s.%v\", " + prev.Path + ", " + prev.KeyVar + ")"
}
next.Name = prev.Name + prev.IndexVar
next.IndexVar = prev.IndexVar + "i"
next.KeyVar = prev.KeyVar + "k"
next.ValueExpression = swag.ToVarName(prev.ValueExpression + "I")
prev.Child = next
}
// found a complex or aliased thing
// hide details from the aliased type and stop recursing
if next.IsAliased || next.IsComplexObject {
next.IsArray = false
next.IsMap = false
next.IsCustomFormatter = false
next.IsComplexObject = true
next.IsAliased = true
break
}
if next.IsInterface || next.IsStream {
next.HasValidations = false
}
prev = next
next = new(GenItems)
switch {
case it.Items != nil:
it = it.Items
case it.AdditionalProperties != nil:
it = it.AdditionalProperties
default:
it = nil
}
}
// propagate HasValidations
var propag func(child *GenItems) bool
propag = func(child *GenItems) bool {
if child == nil {
return false
}
child.HasValidations = child.HasValidations || propag(child.Child)
return child.HasValidations
}
items.HasValidations = propag(items)
// resolve nullability conflicts when declaring body as a map of array of an anonymous complex object
// (e.g. refer to an extra schema type, which is nullable, but not rendered as a pointer in arrays or maps)
// Rule: outer type rules (with IsMapNullOverride), inner types are fixed
var fixNullable func(child *GenItems) string
fixNullable = func(child *GenItems) string {
if !child.IsArray && !child.IsMap {
if child.IsComplexObject {
return child.GoType
}
return ""
}
if innerType := fixNullable(child.Child); innerType != "" {
if child.IsMapNullOverride && child.IsArray {
child.GoType = "[]" + innerType
return child.GoType
}
}
return ""
}
fixNullable(items)
}
return items
}
func (b *codeGenOpBuilder) setBodyParamValidation(p *GenParameter) {
// Determine validation strategy for body param.
//
// Here are the distinct strategies:
// - the body parameter is a model object => delegates
// - the body parameter is an array of model objects => carry on slice validations, then iterate and delegate
// - the body parameter is a map of model objects => iterate and delegate
// - the body parameter is an array of simple objects (including maps)
// - the body parameter is a map of simple objects (including arrays)
if p.IsBodyParam() {
var hasSimpleBodyParams, hasSimpleBodyItems, hasSimpleBodyMap, hasModelBodyParams, hasModelBodyItems, hasModelBodyMap bool
s := p.Schema
if s != nil {
doNot := s.IsInterface || s.IsStream
// composition of primitive fields must be properly identified: hack this through
_, isPrimitive := primitives[s.GoType]
_, isFormatter := customFormatters[s.GoType]
isComposedPrimitive := s.IsPrimitive && !(isPrimitive || isFormatter)
hasSimpleBodyParams = !s.IsComplexObject && !s.IsAliased && !isComposedPrimitive && !doNot
hasModelBodyParams = (s.IsComplexObject || s.IsAliased || isComposedPrimitive) && !doNot
if s.IsArray && s.Items != nil {
it := s.Items
doNot = it.IsInterface || it.IsStream
hasSimpleBodyItems = !it.IsComplexObject && !(it.IsAliased || doNot)
hasModelBodyItems = (it.IsComplexObject || it.IsAliased) && !doNot
}
if s.IsMap && s.AdditionalProperties != nil {
it := s.AdditionalProperties
hasSimpleBodyMap = !it.IsComplexObject && !(it.IsAliased || doNot)
hasModelBodyMap = !hasSimpleBodyMap && !doNot
}
}
// set validation strategy for body param
p.HasSimpleBodyParams = hasSimpleBodyParams
p.HasSimpleBodyItems = hasSimpleBodyItems
p.HasModelBodyParams = hasModelBodyParams
p.HasModelBodyItems = hasModelBodyItems
p.HasModelBodyMap = hasModelBodyMap
p.HasSimpleBodyMap = hasSimpleBodyMap
}
}
// makeSecuritySchemes produces a sorted list of security schemes for this operation
func (b *codeGenOpBuilder) makeSecuritySchemes(receiver string) GenSecuritySchemes {
return gatherSecuritySchemes(b.SecurityDefinitions, b.Name, b.Principal, receiver)
}
// makeSecurityRequirements produces a sorted list of security requirements for this operation.
// As for current, these requirements are not used by codegen (sec. requirement is determined at runtime).
// We keep the order of the slice from the original spec, but sort the inner slice which comes from a map,
// as well as the map of scopes.
func (b *codeGenOpBuilder) makeSecurityRequirements(receiver string) []GenSecurityRequirements {
if b.Security == nil {
// nil (default requirement) is different than [] (no requirement)
return nil
}
securityRequirements := make([]GenSecurityRequirements, 0, len(b.Security))
for _, req := range b.Security {
jointReq := make(GenSecurityRequirements, 0, len(req))
for _, j := range req {
scopes := j.Scopes
sort.Strings(scopes)
jointReq = append(jointReq, GenSecurityRequirement{
Name: j.Name,
Scopes: scopes,
})
}
// sort joint requirements (come from a map in spec)
sort.Sort(jointReq)
securityRequirements = append(securityRequirements, jointReq)
}
return securityRequirements
}
// cloneSchema returns a deep copy of a schema
func (b *codeGenOpBuilder) cloneSchema(schema *spec.Schema) *spec.Schema {
savedSchema := &spec.Schema{}
schemaRep, _ := json.Marshal(schema)
_ = json.Unmarshal(schemaRep, savedSchema)
return savedSchema
}
// saveResolveContext keeps a copy of known definitions and schema to properly roll back on a makeGenSchema() call
// This uses a deep clone the spec document to construct a type resolver which knows about definitions when the making of this operation started,
// and only these definitions. We are not interested in the "original spec", but in the already transformed spec.
func (b *codeGenOpBuilder) saveResolveContext(resolver *typeResolver, schema *spec.Schema) (*typeResolver, *spec.Schema) {
rslv := newTypeResolver(b.GenOpts.LanguageOpts.ManglePackageName(resolver.ModelsPackage, "models"), b.Doc.Pristine())
return rslv, b.cloneSchema(schema)
}
// liftExtraSchemas constructs the schema for an anonymous construct with some ExtraSchemas.
//
// When some ExtraSchemas are produced from something else than a definition,
// this indicates we are not running in fully flattened mode and we need to render
// these ExtraSchemas in the operation's package.
// We need to rebuild the schema with a new type resolver to reflect this change in the
// models package.
func (b *codeGenOpBuilder) liftExtraSchemas(resolver, br *typeResolver, bs *spec.Schema, sc *schemaGenContext) (schema *GenSchema, err error) {
// restore resolving state before previous call to makeGenSchema()
rslv := br
sc.Schema = *bs
pg := sc.shallowClone()
pkg := b.GenOpts.LanguageOpts.ManglePackageName(resolver.ModelsPackage, "models")
pg.TypeResolver = newTypeResolver("", rslv.Doc).withKeepDefinitionsPackage(pkg)
pg.ExtraSchemas = make(map[string]GenSchema, len(sc.ExtraSchemas))
if err = pg.makeGenSchema(); err != nil {
return
}
// lift nested extra schemas (inlined types)
if b.ExtraSchemas == nil {
b.ExtraSchemas = make(map[string]GenSchema, len(pg.ExtraSchemas))
}
for _, v := range pg.ExtraSchemas {
vv := v
if !v.IsStream {
b.ExtraSchemas[vv.Name] = vv
}
}
schema = &pg.GenSchema
return
}
// buildOperationSchema constructs a schema for an operation (for body params or responses).
// It determines if the schema is readily available from the models package,
// or if a schema has to be generated in the operations package (i.e. is anonymous).
// Whenever an anonymous schema needs some extra schemas, we also determine if these extras are
// available from models or must be generated alongside the schema in the operations package.
//
// Duplicate extra schemas are pruned later on, when operations grouping in packages (e.g. from tags) takes place.
func (b *codeGenOpBuilder) buildOperationSchema(schemaPath, containerName, schemaName, receiverName, indexVar string, sch *spec.Schema, resolver *typeResolver) (GenSchema, error) {
var schema GenSchema
if sch == nil {
sch = &spec.Schema{}
}
rslv := resolver
sc := schemaGenContext{
Path: schemaPath,
Name: containerName,
Receiver: receiverName,
ValueExpr: receiverName,
IndexVar: indexVar,
Schema: *sch,
Required: false,
TypeResolver: rslv,
Named: false,
IncludeModel: true,
IncludeValidator: true,
StrictAdditionalProperties: b.GenOpts.StrictAdditionalProperties,
ExtraSchemas: make(map[string]GenSchema),
}
var (
br *typeResolver
bs *spec.Schema
)
// these backups are not needed when sch has name.
if sch.Ref.String() == "" {
br, bs = b.saveResolveContext(rslv, sch)
}
if err := sc.makeGenSchema(); err != nil {
return GenSchema{}, err
}
for alias, pkg := range findImports(&sc.GenSchema) {
b.Imports[alias] = pkg
}
if sch.Ref.String() == "" && len(sc.ExtraSchemas) > 0 {
newSchema, err := b.liftExtraSchemas(resolver, br, bs, &sc)
if err != nil {
return GenSchema{}, err
}
if newSchema != nil {
schema = *newSchema
}
} else {
schema = sc.GenSchema
}
if schema.IsAnonymous {
// a generated name for anonymous schema
// TODO: support x-go-name
hasProperties := len(schema.Properties) > 0
isAllOf := len(schema.AllOf) > 0
isInterface := schema.IsInterface
hasValidations := schema.HasValidations
// for complex anonymous objects, produce an extra schema
if hasProperties || isAllOf {
if b.ExtraSchemas == nil {
b.ExtraSchemas = make(map[string]GenSchema)
}
schema.Name = schemaName
schema.GoType = schemaName
schema.IsAnonymous = false
b.ExtraSchemas[schemaName] = schema
// constructs new schema to refer to the newly created type
schema = GenSchema{}
schema.IsAnonymous = false
schema.IsComplexObject = true
schema.SwaggerType = schemaName
schema.HasValidations = hasValidations
schema.GoType = schemaName
} else if isInterface {
schema = GenSchema{}
schema.IsAnonymous = false
schema.IsComplexObject = false
schema.IsInterface = true
schema.HasValidations = false
schema.GoType = iface
}
}
return schema, nil
}