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 }