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