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 }