github.com/josephspurrier/go-swagger@v0.2.1-0.20221129144919-1f672a142a00/codescan/responses.go (about)

     1  package codescan
     2  
     3  import (
     4  	"fmt"
     5  	"go/ast"
     6  	"go/types"
     7  	"strings"
     8  
     9  	"github.com/pkg/errors"
    10  
    11  	"golang.org/x/tools/go/ast/astutil"
    12  
    13  	"github.com/go-openapi/spec"
    14  )
    15  
    16  type responseTypable struct {
    17  	in       string
    18  	header   *spec.Header
    19  	response *spec.Response
    20  }
    21  
    22  func (ht responseTypable) Level() int { return 0 }
    23  
    24  func (ht responseTypable) Typed(tpe, format string) {
    25  	ht.header.Typed(tpe, format)
    26  }
    27  
    28  func bodyTypable(in string, schema *spec.Schema) (swaggerTypable, *spec.Schema) {
    29  	if in == "body" {
    30  		// get the schema for items on the schema property
    31  		if schema == nil {
    32  			schema = new(spec.Schema)
    33  		}
    34  		if schema.Items == nil {
    35  			schema.Items = new(spec.SchemaOrArray)
    36  		}
    37  		if schema.Items.Schema == nil {
    38  			schema.Items.Schema = new(spec.Schema)
    39  		}
    40  		schema.Typed("array", "")
    41  		return schemaTypable{schema.Items.Schema, 1}, schema
    42  	}
    43  	return nil, nil
    44  }
    45  
    46  func (ht responseTypable) Items() swaggerTypable {
    47  	bdt, schema := bodyTypable(ht.in, ht.response.Schema)
    48  	if bdt != nil {
    49  		ht.response.Schema = schema
    50  		return bdt
    51  	}
    52  
    53  	if ht.header.Items == nil {
    54  		ht.header.Items = new(spec.Items)
    55  	}
    56  	ht.header.Type = "array"
    57  	return itemsTypable{ht.header.Items, 1}
    58  }
    59  
    60  func (ht responseTypable) SetRef(ref spec.Ref) {
    61  	// having trouble seeing the usefulness of this one here
    62  	ht.Schema().Ref = ref
    63  }
    64  
    65  func (ht responseTypable) Schema() *spec.Schema {
    66  	if ht.response.Schema == nil {
    67  		ht.response.Schema = new(spec.Schema)
    68  	}
    69  	return ht.response.Schema
    70  }
    71  
    72  func (ht responseTypable) SetSchema(schema *spec.Schema) {
    73  	ht.response.Schema = schema
    74  }
    75  
    76  func (ht responseTypable) CollectionOf(items *spec.Items, format string) {
    77  	ht.header.CollectionOf(items, format)
    78  }
    79  
    80  func (ht responseTypable) AddExtension(key string, value interface{}) {
    81  	ht.response.AddExtension(key, value)
    82  }
    83  
    84  func (ht responseTypable) WithEnum(values ...interface{}) {
    85  	ht.header.WithEnum(values)
    86  }
    87  
    88  func (ht responseTypable) WithEnumDescription(desc string) {
    89  	// no
    90  }
    91  
    92  type headerValidations struct {
    93  	current *spec.Header
    94  }
    95  
    96  func (sv headerValidations) SetMaximum(val float64, exclusive bool) {
    97  	sv.current.Maximum = &val
    98  	sv.current.ExclusiveMaximum = exclusive
    99  }
   100  func (sv headerValidations) SetMinimum(val float64, exclusive bool) {
   101  	sv.current.Minimum = &val
   102  	sv.current.ExclusiveMinimum = exclusive
   103  }
   104  func (sv headerValidations) SetMultipleOf(val float64)      { sv.current.MultipleOf = &val }
   105  func (sv headerValidations) SetMinItems(val int64)          { sv.current.MinItems = &val }
   106  func (sv headerValidations) SetMaxItems(val int64)          { sv.current.MaxItems = &val }
   107  func (sv headerValidations) SetMinLength(val int64)         { sv.current.MinLength = &val }
   108  func (sv headerValidations) SetMaxLength(val int64)         { sv.current.MaxLength = &val }
   109  func (sv headerValidations) SetPattern(val string)          { sv.current.Pattern = val }
   110  func (sv headerValidations) SetUnique(val bool)             { sv.current.UniqueItems = val }
   111  func (sv headerValidations) SetCollectionFormat(val string) { sv.current.CollectionFormat = val }
   112  func (sv headerValidations) SetEnum(val string) {
   113  	sv.current.Enum = parseEnum(val, &spec.SimpleSchema{Type: sv.current.Type, Format: sv.current.Format})
   114  }
   115  func (sv headerValidations) SetDefault(val interface{}) { sv.current.Default = val }
   116  func (sv headerValidations) SetExample(val interface{}) { sv.current.Example = val }
   117  
   118  type responseBuilder struct {
   119  	ctx       *scanCtx
   120  	decl      *entityDecl
   121  	postDecls []*entityDecl
   122  }
   123  
   124  func (r *responseBuilder) Build(responses map[string]spec.Response) error {
   125  	// check if there is a swagger:response tag that is followed by one or more words,
   126  	// these words are the ids of the operations this parameter struct applies to
   127  	// once type name is found convert it to a schema, by looking up the schema in the
   128  	// parameters dictionary that got passed into this parse method
   129  
   130  	name, _ := r.decl.ResponseNames()
   131  	response := responses[name]
   132  	debugLog("building response: %s", name)
   133  
   134  	// analyze doc comment for the model
   135  	sp := new(sectionedParser)
   136  	sp.setDescription = func(lines []string) { response.Description = joinDropLast(lines) }
   137  	if err := sp.Parse(r.decl.Comments); err != nil {
   138  		return err
   139  	}
   140  
   141  	// analyze struct body for fields etc
   142  	// each exported struct field:
   143  	// * gets a type mapped to a go primitive
   144  	// * perhaps gets a format
   145  	// * has to document the validations that apply for the type and the field
   146  	// * when the struct field points to a model it becomes a ref: #/definitions/ModelName
   147  	// * comments that aren't tags is used as the description
   148  	if err := r.buildFromType(r.decl.Type, &response, make(map[string]bool)); err != nil {
   149  		return err
   150  	}
   151  	responses[name] = response
   152  	return nil
   153  }
   154  
   155  func (r *responseBuilder) buildFromField(fld *types.Var, tpe types.Type, typable swaggerTypable, seen map[string]bool) error {
   156  	debugLog("build from field %s: %T", fld.Name(), tpe)
   157  	switch ftpe := tpe.(type) {
   158  	case *types.Basic:
   159  		return swaggerSchemaForType(ftpe.Name(), typable)
   160  	case *types.Struct:
   161  		sb := schemaBuilder{
   162  			decl: r.decl,
   163  			ctx:  r.ctx,
   164  		}
   165  		if err := sb.buildFromType(tpe, typable); err != nil {
   166  			return err
   167  		}
   168  		r.postDecls = append(r.postDecls, sb.postDecls...)
   169  		return nil
   170  	case *types.Pointer:
   171  		return r.buildFromField(fld, ftpe.Elem(), typable, seen)
   172  	case *types.Interface:
   173  		sb := schemaBuilder{
   174  			decl: r.decl,
   175  			ctx:  r.ctx,
   176  		}
   177  		if err := sb.buildFromType(tpe, typable); err != nil {
   178  			return err
   179  		}
   180  		r.postDecls = append(r.postDecls, sb.postDecls...)
   181  		return nil
   182  	case *types.Array:
   183  		return r.buildFromField(fld, ftpe.Elem(), typable.Items(), seen)
   184  	case *types.Slice:
   185  		return r.buildFromField(fld, ftpe.Elem(), typable.Items(), seen)
   186  	case *types.Map:
   187  		schema := new(spec.Schema)
   188  		typable.Schema().Typed("object", "").AdditionalProperties = &spec.SchemaOrBool{
   189  			Schema: schema,
   190  		}
   191  		sb := schemaBuilder{
   192  			decl: r.decl,
   193  			ctx:  r.ctx,
   194  		}
   195  		if err := sb.buildFromType(ftpe.Elem(), schemaTypable{schema, typable.Level() + 1}); err != nil {
   196  			return err
   197  		}
   198  		r.postDecls = append(r.postDecls, sb.postDecls...)
   199  		return nil
   200  	case *types.Named:
   201  		if decl, found := r.ctx.DeclForType(ftpe.Obj().Type()); found {
   202  			if decl.Type.Obj().Pkg().Path() == "time" && decl.Type.Obj().Name() == "Time" {
   203  				typable.Typed("string", "date-time")
   204  				return nil
   205  			}
   206  			if sfnm, isf := strfmtName(decl.Comments); isf {
   207  				typable.Typed("string", sfnm)
   208  				return nil
   209  			}
   210  			sb := &schemaBuilder{ctx: r.ctx, decl: decl}
   211  			sb.inferNames()
   212  			if err := sb.buildFromType(decl.Type, typable); err != nil {
   213  				return err
   214  			}
   215  			r.postDecls = append(r.postDecls, sb.postDecls...)
   216  			return nil
   217  		}
   218  		return errors.Errorf("unable to find package and source file for: %s", ftpe.String())
   219  	default:
   220  		return errors.Errorf("unknown type for %s: %T", fld.String(), fld.Type())
   221  	}
   222  }
   223  
   224  func (r *responseBuilder) buildFromType(otpe types.Type, resp *spec.Response, seen map[string]bool) error {
   225  	switch tpe := otpe.(type) {
   226  	case *types.Pointer:
   227  		return r.buildFromType(tpe.Elem(), resp, seen)
   228  	case *types.Named:
   229  		o := tpe.Obj()
   230  		switch stpe := o.Type().Underlying().(type) {
   231  		case *types.Struct:
   232  			debugLog("build from type %s: %T", tpe.Obj().Name(), otpe)
   233  			if decl, found := r.ctx.DeclForType(o.Type()); found {
   234  				return r.buildFromStruct(decl, stpe, resp, seen)
   235  			}
   236  			return r.buildFromStruct(r.decl, stpe, resp, seen)
   237  		default:
   238  			if decl, found := r.ctx.DeclForType(o.Type()); found {
   239  				var schema spec.Schema
   240  				typable := schemaTypable{schema: &schema, level: 0}
   241  
   242  				if decl.Type.Obj().Pkg().Path() == "time" && decl.Type.Obj().Name() == "Time" {
   243  					typable.Typed("string", "date-time")
   244  					return nil
   245  				}
   246  				if sfnm, isf := strfmtName(decl.Comments); isf {
   247  					typable.Typed("string", sfnm)
   248  					return nil
   249  				}
   250  				sb := &schemaBuilder{ctx: r.ctx, decl: decl}
   251  				sb.inferNames()
   252  				if err := sb.buildFromType(tpe.Underlying(), typable); err != nil {
   253  					return err
   254  				}
   255  				resp.WithSchema(&schema)
   256  				r.postDecls = append(r.postDecls, sb.postDecls...)
   257  				return nil
   258  			}
   259  			return errors.Errorf("responses can only be structs, did you mean for %s to be the response body?", otpe.String())
   260  		}
   261  	default:
   262  		return errors.New("anonymous types are currently not supported for responses")
   263  	}
   264  }
   265  
   266  func (r *responseBuilder) buildFromStruct(decl *entityDecl, tpe *types.Struct, resp *spec.Response, seen map[string]bool) error {
   267  	if tpe.NumFields() == 0 {
   268  		return nil
   269  	}
   270  
   271  	for i := 0; i < tpe.NumFields(); i++ {
   272  		fld := tpe.Field(i)
   273  		if fld.Embedded() {
   274  
   275  			if err := r.buildFromType(fld.Type(), resp, seen); err != nil {
   276  				return err
   277  			}
   278  			continue
   279  		}
   280  		if fld.Anonymous() {
   281  			debugLog("skipping anonymous field")
   282  			continue
   283  		}
   284  
   285  		tg := tpe.Tag(i)
   286  
   287  		var afld *ast.Field
   288  		ans, _ := astutil.PathEnclosingInterval(decl.File, fld.Pos(), fld.Pos())
   289  		for _, an := range ans {
   290  			at, valid := an.(*ast.Field)
   291  			if !valid {
   292  				continue
   293  			}
   294  
   295  			debugLog("field %s: %s(%T) [%q] ==> %s", fld.Name(), fld.Type().String(), fld.Type(), tg, at.Doc.Text())
   296  			afld = at
   297  			break
   298  		}
   299  
   300  		if afld == nil {
   301  			debugLog("can't find source associated with %s for %s", fld.String(), tpe.String())
   302  			continue
   303  		}
   304  
   305  		// if the field is annotated with swagger:ignore, ignore it
   306  		if ignored(afld.Doc) {
   307  			continue
   308  		}
   309  
   310  		name, ignore, _, err := parseJSONTag(afld)
   311  		if err != nil {
   312  			return err
   313  		}
   314  		if ignore {
   315  			continue
   316  		}
   317  
   318  		var in string
   319  		// scan for param location first, this changes some behavior down the line
   320  		if afld.Doc != nil {
   321  			for _, cmt := range afld.Doc.List {
   322  				for _, line := range strings.Split(cmt.Text, "\n") {
   323  					matches := rxIn.FindStringSubmatch(line)
   324  					if len(matches) > 0 && len(strings.TrimSpace(matches[1])) > 0 {
   325  						in = strings.TrimSpace(matches[1])
   326  					}
   327  				}
   328  			}
   329  		}
   330  
   331  		ps := resp.Headers[name]
   332  
   333  		// support swagger:file for response
   334  		// An API operation can return a file, such as an image or PDF. In this case,
   335  		// define the response schema with type: file and specify the appropriate MIME types in the produces section.
   336  		if afld.Doc != nil && fileParam(afld.Doc) {
   337  			resp.Schema = &spec.Schema{}
   338  			resp.Schema.Typed("file", "")
   339  		} else if err := r.buildFromField(fld, fld.Type(), responseTypable{in, &ps, resp}, seen); err != nil {
   340  			return err
   341  		}
   342  
   343  		if strfmtName, ok := strfmtName(afld.Doc); ok {
   344  			ps.Typed("string", strfmtName)
   345  		}
   346  
   347  		sp := new(sectionedParser)
   348  		sp.setDescription = func(lines []string) { ps.Description = joinDropLast(lines) }
   349  		sp.taggers = []tagParser{
   350  			newSingleLineTagParser("maximum", &setMaximum{headerValidations{&ps}, rxf(rxMaximumFmt, "")}),
   351  			newSingleLineTagParser("minimum", &setMinimum{headerValidations{&ps}, rxf(rxMinimumFmt, "")}),
   352  			newSingleLineTagParser("multipleOf", &setMultipleOf{headerValidations{&ps}, rxf(rxMultipleOfFmt, "")}),
   353  			newSingleLineTagParser("minLength", &setMinLength{headerValidations{&ps}, rxf(rxMinLengthFmt, "")}),
   354  			newSingleLineTagParser("maxLength", &setMaxLength{headerValidations{&ps}, rxf(rxMaxLengthFmt, "")}),
   355  			newSingleLineTagParser("pattern", &setPattern{headerValidations{&ps}, rxf(rxPatternFmt, "")}),
   356  			newSingleLineTagParser("collectionFormat", &setCollectionFormat{headerValidations{&ps}, rxf(rxCollectionFormatFmt, "")}),
   357  			newSingleLineTagParser("minItems", &setMinItems{headerValidations{&ps}, rxf(rxMinItemsFmt, "")}),
   358  			newSingleLineTagParser("maxItems", &setMaxItems{headerValidations{&ps}, rxf(rxMaxItemsFmt, "")}),
   359  			newSingleLineTagParser("unique", &setUnique{headerValidations{&ps}, rxf(rxUniqueFmt, "")}),
   360  			newSingleLineTagParser("enum", &setEnum{headerValidations{&ps}, rxf(rxEnumFmt, "")}),
   361  			newSingleLineTagParser("default", &setDefault{&ps.SimpleSchema, headerValidations{&ps}, rxf(rxDefaultFmt, "")}),
   362  			newSingleLineTagParser("example", &setExample{&ps.SimpleSchema, headerValidations{&ps}, rxf(rxExampleFmt, "")}),
   363  		}
   364  		itemsTaggers := func(items *spec.Items, level int) []tagParser {
   365  			// the expression is 1-index based not 0-index
   366  			itemsPrefix := fmt.Sprintf(rxItemsPrefixFmt, level+1)
   367  
   368  			return []tagParser{
   369  				newSingleLineTagParser(fmt.Sprintf("items%dMaximum", level), &setMaximum{itemsValidations{items}, rxf(rxMaximumFmt, itemsPrefix)}),
   370  				newSingleLineTagParser(fmt.Sprintf("items%dMinimum", level), &setMinimum{itemsValidations{items}, rxf(rxMinimumFmt, itemsPrefix)}),
   371  				newSingleLineTagParser(fmt.Sprintf("items%dMultipleOf", level), &setMultipleOf{itemsValidations{items}, rxf(rxMultipleOfFmt, itemsPrefix)}),
   372  				newSingleLineTagParser(fmt.Sprintf("items%dMinLength", level), &setMinLength{itemsValidations{items}, rxf(rxMinLengthFmt, itemsPrefix)}),
   373  				newSingleLineTagParser(fmt.Sprintf("items%dMaxLength", level), &setMaxLength{itemsValidations{items}, rxf(rxMaxLengthFmt, itemsPrefix)}),
   374  				newSingleLineTagParser(fmt.Sprintf("items%dPattern", level), &setPattern{itemsValidations{items}, rxf(rxPatternFmt, itemsPrefix)}),
   375  				newSingleLineTagParser(fmt.Sprintf("items%dCollectionFormat", level), &setCollectionFormat{itemsValidations{items}, rxf(rxCollectionFormatFmt, itemsPrefix)}),
   376  				newSingleLineTagParser(fmt.Sprintf("items%dMinItems", level), &setMinItems{itemsValidations{items}, rxf(rxMinItemsFmt, itemsPrefix)}),
   377  				newSingleLineTagParser(fmt.Sprintf("items%dMaxItems", level), &setMaxItems{itemsValidations{items}, rxf(rxMaxItemsFmt, itemsPrefix)}),
   378  				newSingleLineTagParser(fmt.Sprintf("items%dUnique", level), &setUnique{itemsValidations{items}, rxf(rxUniqueFmt, itemsPrefix)}),
   379  				newSingleLineTagParser(fmt.Sprintf("items%dEnum", level), &setEnum{itemsValidations{items}, rxf(rxEnumFmt, itemsPrefix)}),
   380  				newSingleLineTagParser(fmt.Sprintf("items%dDefault", level), &setDefault{&items.SimpleSchema, itemsValidations{items}, rxf(rxDefaultFmt, itemsPrefix)}),
   381  				newSingleLineTagParser(fmt.Sprintf("items%dExample", level), &setExample{&items.SimpleSchema, itemsValidations{items}, rxf(rxExampleFmt, itemsPrefix)}),
   382  			}
   383  		}
   384  
   385  		var parseArrayTypes func(expr ast.Expr, items *spec.Items, level int) ([]tagParser, error)
   386  		parseArrayTypes = func(expr ast.Expr, items *spec.Items, level int) ([]tagParser, error) {
   387  			if items == nil {
   388  				return []tagParser{}, nil
   389  			}
   390  			switch iftpe := expr.(type) {
   391  			case *ast.ArrayType:
   392  				eleTaggers := itemsTaggers(items, level)
   393  				sp.taggers = append(eleTaggers, sp.taggers...)
   394  				otherTaggers, err := parseArrayTypes(iftpe.Elt, items.Items, level+1)
   395  				if err != nil {
   396  					return nil, err
   397  				}
   398  				return otherTaggers, nil
   399  			case *ast.Ident:
   400  				taggers := []tagParser{}
   401  				if iftpe.Obj == nil {
   402  					taggers = itemsTaggers(items, level)
   403  				}
   404  				otherTaggers, err := parseArrayTypes(expr, items.Items, level+1)
   405  				if err != nil {
   406  					return nil, err
   407  				}
   408  				return append(taggers, otherTaggers...), nil
   409  			case *ast.SelectorExpr:
   410  				otherTaggers, err := parseArrayTypes(iftpe.Sel, items.Items, level+1)
   411  				if err != nil {
   412  					return nil, err
   413  				}
   414  				return otherTaggers, nil
   415  			case *ast.StarExpr:
   416  				otherTaggers, err := parseArrayTypes(iftpe.X, items, level)
   417  				if err != nil {
   418  					return nil, err
   419  				}
   420  				return otherTaggers, nil
   421  			default:
   422  				return nil, fmt.Errorf("unknown field type ele for %q", name)
   423  			}
   424  		}
   425  		// check if this is a primitive, if so parse the validations from the
   426  		// doc comments of the slice declaration.
   427  		if ftped, ok := afld.Type.(*ast.ArrayType); ok {
   428  			taggers, err := parseArrayTypes(ftped.Elt, ps.Items, 0)
   429  			if err != nil {
   430  				return err
   431  			}
   432  			sp.taggers = append(taggers, sp.taggers...)
   433  		}
   434  
   435  		if err := sp.Parse(afld.Doc); err != nil {
   436  			return err
   437  		}
   438  
   439  		if in != "body" {
   440  			seen[name] = true
   441  			if resp.Headers == nil {
   442  				resp.Headers = make(map[string]spec.Header)
   443  			}
   444  			resp.Headers[name] = ps
   445  		}
   446  	}
   447  
   448  	for k := range resp.Headers {
   449  		if !seen[k] {
   450  			delete(resp.Headers, k)
   451  		}
   452  	}
   453  	return nil
   454  }