|
- // 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
- }
|