github.com/ManabuSeki/goa-v1@v1.4.3/goagen/gen_swagger/swagger.go (about) 1 package genswagger 2 3 import ( 4 "encoding/json" 5 "fmt" 6 "sort" 7 "strconv" 8 "strings" 9 10 "github.com/goadesign/goa/design" 11 "github.com/goadesign/goa/dslengine" 12 genschema "github.com/goadesign/goa/goagen/gen_schema" 13 ) 14 15 type ( 16 // Swagger represents an instance of a swagger object. 17 // See https://swagger.io/specification/ 18 Swagger struct { 19 Swagger string `json:"swagger,omitempty"` 20 Info *Info `json:"info,omitempty"` 21 Host string `json:"host,omitempty"` 22 BasePath string `json:"basePath,omitempty"` 23 Schemes []string `json:"schemes,omitempty"` 24 Consumes []string `json:"consumes,omitempty"` 25 Produces []string `json:"produces,omitempty"` 26 Paths map[string]interface{} `json:"paths"` 27 Definitions map[string]*genschema.JSONSchema `json:"definitions,omitempty"` 28 Parameters map[string]*Parameter `json:"parameters,omitempty"` 29 Responses map[string]*Response `json:"responses,omitempty"` 30 SecurityDefinitions map[string]*SecurityDefinition `json:"securityDefinitions,omitempty"` 31 Tags []*Tag `json:"tags,omitempty"` 32 ExternalDocs *ExternalDocs `json:"externalDocs,omitempty"` 33 } 34 35 // Info provides metadata about the API. The metadata can be used by the clients if needed, 36 // and can be presented in the Swagger-UI for convenience. 37 Info struct { 38 Title string `json:"title,omitempty"` 39 Description string `json:"description,omitempty"` 40 TermsOfService string `json:"termsOfService,omitempty"` 41 Contact *design.ContactDefinition `json:"contact,omitempty"` 42 License *design.LicenseDefinition `json:"license,omitempty"` 43 Version string `json:"version"` 44 Extensions map[string]interface{} `json:"-"` 45 } 46 47 // Path holds the relative paths to the individual endpoints. 48 Path struct { 49 // Ref allows for an external definition of this path item. 50 Ref string `json:"$ref,omitempty"` 51 // Get defines a GET operation on this path. 52 Get *Operation `json:"get,omitempty"` 53 // Put defines a PUT operation on this path. 54 Put *Operation `json:"put,omitempty"` 55 // Post defines a POST operation on this path. 56 Post *Operation `json:"post,omitempty"` 57 // Delete defines a DELETE operation on this path. 58 Delete *Operation `json:"delete,omitempty"` 59 // Options defines a OPTIONS operation on this path. 60 Options *Operation `json:"options,omitempty"` 61 // Head defines a HEAD operation on this path. 62 Head *Operation `json:"head,omitempty"` 63 // Patch defines a PATCH operation on this path. 64 Patch *Operation `json:"patch,omitempty"` 65 // Parameters is the list of parameters that are applicable for all the operations 66 // described under this path. 67 Parameters []*Parameter `json:"parameters,omitempty"` 68 // Extensions defines the swagger extensions. 69 Extensions map[string]interface{} `json:"-"` 70 } 71 72 // Operation describes a single API operation on a path. 73 Operation struct { 74 // Tags is a list of tags for API documentation control. Tags can be used for 75 // logical grouping of operations by resources or any other qualifier. 76 Tags []string `json:"tags,omitempty"` 77 // Summary is a short summary of what the operation does. For maximum readability 78 // in the swagger-ui, this field should be less than 120 characters. 79 Summary string `json:"summary,omitempty"` 80 // Description is a verbose explanation of the operation behavior. 81 // GFM syntax can be used for rich text representation. 82 Description string `json:"description,omitempty"` 83 // ExternalDocs points to additional external documentation for this operation. 84 ExternalDocs *ExternalDocs `json:"externalDocs,omitempty"` 85 // OperationID is a unique string used to identify the operation. 86 OperationID string `json:"operationId,omitempty"` 87 // Consumes is a list of MIME types the operation can consume. 88 Consumes []string `json:"consumes,omitempty"` 89 // Produces is a list of MIME types the operation can produce. 90 Produces []string `json:"produces,omitempty"` 91 // Parameters is a list of parameters that are applicable for this operation. 92 Parameters []*Parameter `json:"parameters,omitempty"` 93 // Responses is the list of possible responses as they are returned from executing 94 // this operation. 95 Responses map[string]*Response `json:"responses,omitempty"` 96 // Schemes is the transfer protocol for the operation. 97 Schemes []string `json:"schemes,omitempty"` 98 // Deprecated declares this operation to be deprecated. 99 Deprecated bool `json:"deprecated,omitempty"` 100 // Secury is a declaration of which security schemes are applied for this operation. 101 Security []map[string][]string `json:"security,omitempty"` 102 // Extensions defines the swagger extensions. 103 Extensions map[string]interface{} `json:"-"` 104 } 105 106 // Parameter describes a single operation parameter. 107 Parameter struct { 108 // Name of the parameter. Parameter names are case sensitive. 109 Name string `json:"name"` 110 // In is the location of the parameter. 111 // Possible values are "query", "header", "path", "formData" or "body". 112 In string `json:"in"` 113 // Description is`a brief description of the parameter. 114 // GFM syntax can be used for rich text representation. 115 Description string `json:"description,omitempty"` 116 // Required determines whether this parameter is mandatory. 117 Required bool `json:"required"` 118 // Schema defining the type used for the body parameter, only if "in" is body 119 Schema *genschema.JSONSchema `json:"schema,omitempty"` 120 121 // properties below only apply if "in" is not body 122 123 // Type of the parameter. Since the parameter is not located at the request body, 124 // it is limited to simple types (that is, not an object). 125 Type string `json:"type,omitempty"` 126 // Format is the extending format for the previously mentioned type. 127 Format string `json:"format,omitempty"` 128 // AllowEmptyValue sets the ability to pass empty-valued parameters. 129 // This is valid only for either query or formData parameters and allows you to 130 // send a parameter with a name only or an empty value. Default value is false. 131 AllowEmptyValue bool `json:"allowEmptyValue,omitempty"` 132 // Items describes the type of items in the array if type is "array". 133 Items *Items `json:"items,omitempty"` 134 // CollectionFormat determines the format of the array if type array is used. 135 // Possible values are csv, ssv, tsv, pipes and multi. 136 CollectionFormat string `json:"collectionFormat,omitempty"` 137 // Default declares the value of the parameter that the server will use if none is 138 // provided, for example a "count" to control the number of results per page might 139 // default to 100 if not supplied by the client in the request. 140 Default interface{} `json:"default,omitempty"` 141 Maximum *float64 `json:"maximum,omitempty"` 142 ExclusiveMaximum bool `json:"exclusiveMaximum,omitempty"` 143 Minimum *float64 `json:"minimum,omitempty"` 144 ExclusiveMinimum bool `json:"exclusiveMinimum,omitempty"` 145 MaxLength *int `json:"maxLength,omitempty"` 146 MinLength *int `json:"minLength,omitempty"` 147 Pattern string `json:"pattern,omitempty"` 148 MaxItems *int `json:"maxItems,omitempty"` 149 MinItems *int `json:"minItems,omitempty"` 150 UniqueItems bool `json:"uniqueItems,omitempty"` 151 Enum []interface{} `json:"enum,omitempty"` 152 MultipleOf float64 `json:"multipleOf,omitempty"` 153 // Extensions defines the swagger extensions. 154 Extensions map[string]interface{} `json:"-"` 155 } 156 157 // Response describes an operation response. 158 Response struct { 159 // Description of the response. GFM syntax can be used for rich text representation. 160 Description string `json:"description,omitempty"` 161 // Schema is a definition of the response structure. It can be a primitive, 162 // an array or an object. If this field does not exist, it means no content is 163 // returned as part of the response. As an extension to the Schema Object, its root 164 // type value may also be "file". 165 Schema *genschema.JSONSchema `json:"schema,omitempty"` 166 // Headers is a list of headers that are sent with the response. 167 Headers map[string]*Header `json:"headers,omitempty"` 168 // Ref references a global API response. 169 // This field is exclusive with the other fields of Response. 170 Ref string `json:"$ref,omitempty"` 171 // Extensions defines the swagger extensions. 172 Extensions map[string]interface{} `json:"-"` 173 } 174 175 // Header represents a header parameter. 176 Header struct { 177 // Description is`a brief description of the parameter. 178 // GFM syntax can be used for rich text representation. 179 Description string `json:"description,omitempty"` 180 // Type of the header. it is limited to simple types (that is, not an object). 181 Type string `json:"type,omitempty"` 182 // Format is the extending format for the previously mentioned type. 183 Format string `json:"format,omitempty"` 184 // Items describes the type of items in the array if type is "array". 185 Items *Items `json:"items,omitempty"` 186 // CollectionFormat determines the format of the array if type array is used. 187 // Possible values are csv, ssv, tsv, pipes and multi. 188 CollectionFormat string `json:"collectionFormat,omitempty"` 189 // Default declares the value of the parameter that the server will use if none is 190 // provided, for example a "count" to control the number of results per page might 191 // default to 100 if not supplied by the client in the request. 192 Default interface{} `json:"default,omitempty"` 193 Maximum *float64 `json:"maximum,omitempty"` 194 ExclusiveMaximum bool `json:"exclusiveMaximum,omitempty"` 195 Minimum *float64 `json:"minimum,omitempty"` 196 ExclusiveMinimum bool `json:"exclusiveMinimum,omitempty"` 197 MaxLength *int `json:"maxLength,omitempty"` 198 MinLength *int `json:"minLength,omitempty"` 199 Pattern string `json:"pattern,omitempty"` 200 MaxItems *int `json:"maxItems,omitempty"` 201 MinItems *int `json:"minItems,omitempty"` 202 UniqueItems bool `json:"uniqueItems,omitempty"` 203 Enum []interface{} `json:"enum,omitempty"` 204 MultipleOf float64 `json:"multipleOf,omitempty"` 205 } 206 207 // SecurityDefinition allows the definition of a security scheme that can be used by the 208 // operations. Supported schemes are basic authentication, an API key (either as a header or 209 // as a query parameter) and OAuth2's common flows (implicit, password, application and 210 // access code). 211 SecurityDefinition struct { 212 // Type of the security scheme. Valid values are "basic", "apiKey" or "oauth2". 213 Type string `json:"type"` 214 // Description for security scheme 215 Description string `json:"description,omitempty"` 216 // Name of the header or query parameter to be used when type is "apiKey". 217 Name string `json:"name,omitempty"` 218 // In is the location of the API key when type is "apiKey". 219 // Valid values are "query" or "header". 220 In string `json:"in,omitempty"` 221 // Flow is the flow used by the OAuth2 security scheme when type is "oauth2" 222 // Valid values are "implicit", "password", "application" or "accessCode". 223 Flow string `json:"flow,omitempty"` 224 // The oauth2 authorization URL to be used for this flow. 225 AuthorizationURL string `json:"authorizationUrl,omitempty"` 226 // TokenURL is the token URL to be used for this flow. 227 TokenURL string `json:"tokenUrl,omitempty"` 228 // Scopes list the available scopes for the OAuth2 security scheme. 229 Scopes map[string]string `json:"scopes,omitempty"` 230 // Extensions defines the swagger extensions. 231 Extensions map[string]interface{} `json:"-"` 232 } 233 234 // Scope corresponds to an available scope for an OAuth2 security scheme. 235 Scope struct { 236 // Description for scope 237 Description string `json:"description,omitempty"` 238 } 239 240 // ExternalDocs allows referencing an external resource for extended documentation. 241 ExternalDocs struct { 242 // Description is a short description of the target documentation. 243 // GFM syntax can be used for rich text representation. 244 Description string `json:"description,omitempty"` 245 // URL for the target documentation. 246 URL string `json:"url"` 247 } 248 249 // Items is a limited subset of JSON-Schema's items object. It is used by parameter 250 // definitions that are not located in "body". 251 Items struct { 252 // Type of the items. it is limited to simple types (that is, not an object). 253 Type string `json:"type,omitempty"` 254 // Format is the extending format for the previously mentioned type. 255 Format string `json:"format,omitempty"` 256 // Items describes the type of items in the array if type is "array". 257 Items *Items `json:"items,omitempty"` 258 // CollectionFormat determines the format of the array if type array is used. 259 // Possible values are csv, ssv, tsv, pipes and multi. 260 CollectionFormat string `json:"collectionFormat,omitempty"` 261 // Default declares the value of the parameter that the server will use if none is 262 // provided, for example a "count" to control the number of results per page might 263 // default to 100 if not supplied by the client in the request. 264 Default interface{} `json:"default,omitempty"` 265 Maximum *float64 `json:"maximum,omitempty"` 266 ExclusiveMaximum bool `json:"exclusiveMaximum,omitempty"` 267 Minimum *float64 `json:"minimum,omitempty"` 268 ExclusiveMinimum bool `json:"exclusiveMinimum,omitempty"` 269 MaxLength *int `json:"maxLength,omitempty"` 270 MinLength *int `json:"minLength,omitempty"` 271 Pattern string `json:"pattern,omitempty"` 272 MaxItems *int `json:"maxItems,omitempty"` 273 MinItems *int `json:"minItems,omitempty"` 274 UniqueItems bool `json:"uniqueItems,omitempty"` 275 Enum []interface{} `json:"enum,omitempty"` 276 MultipleOf float64 `json:"multipleOf,omitempty"` 277 } 278 279 // Tag allows adding meta data to a single tag that is used by the Operation Object. It is 280 // not mandatory to have a Tag Object per tag used there. 281 Tag struct { 282 // Name of the tag. 283 Name string `json:"name,omitempty"` 284 // Description is a short description of the tag. 285 // GFM syntax can be used for rich text representation. 286 Description string `json:"description,omitempty"` 287 // ExternalDocs is additional external documentation for this tag. 288 ExternalDocs *ExternalDocs `json:"externalDocs,omitempty"` 289 // Extensions defines the swagger extensions. 290 Extensions map[string]interface{} `json:"-"` 291 } 292 293 // These types are used in marshalJSON() to avoid recursive call of json.Marshal(). 294 _Info Info 295 _Path Path 296 _Operation Operation 297 _Parameter Parameter 298 _Response Response 299 _SecurityDefinition SecurityDefinition 300 _Tag Tag 301 ) 302 303 func marshalJSON(v interface{}, extensions map[string]interface{}) ([]byte, error) { 304 marshaled, err := json.Marshal(v) 305 if err != nil { 306 return nil, err 307 } 308 if len(extensions) == 0 { 309 return marshaled, nil 310 } 311 var unmarshaled interface{} 312 if err := json.Unmarshal(marshaled, &unmarshaled); err != nil { 313 return nil, err 314 } 315 asserted := unmarshaled.(map[string]interface{}) 316 for k, v := range extensions { 317 asserted[k] = v 318 } 319 merged, err := json.Marshal(asserted) 320 if err != nil { 321 return nil, err 322 } 323 return merged, nil 324 } 325 326 // MarshalJSON returns the JSON encoding of i. 327 func (i Info) MarshalJSON() ([]byte, error) { 328 return marshalJSON(_Info(i), i.Extensions) 329 } 330 331 // MarshalJSON returns the JSON encoding of p. 332 func (p Path) MarshalJSON() ([]byte, error) { 333 return marshalJSON(_Path(p), p.Extensions) 334 } 335 336 // MarshalJSON returns the JSON encoding of o. 337 func (o Operation) MarshalJSON() ([]byte, error) { 338 return marshalJSON(_Operation(o), o.Extensions) 339 } 340 341 // MarshalJSON returns the JSON encoding of p. 342 func (p Parameter) MarshalJSON() ([]byte, error) { 343 return marshalJSON(_Parameter(p), p.Extensions) 344 } 345 346 // MarshalJSON returns the JSON encoding of r. 347 func (r Response) MarshalJSON() ([]byte, error) { 348 return marshalJSON(_Response(r), r.Extensions) 349 } 350 351 // MarshalJSON returns the JSON encoding of s. 352 func (s SecurityDefinition) MarshalJSON() ([]byte, error) { 353 return marshalJSON(_SecurityDefinition(s), s.Extensions) 354 } 355 356 // MarshalJSON returns the JSON encoding of t. 357 func (t Tag) MarshalJSON() ([]byte, error) { 358 return marshalJSON(_Tag(t), t.Extensions) 359 } 360 361 // New creates a Swagger spec from an API definition. 362 func New(api *design.APIDefinition) (*Swagger, error) { 363 if api == nil { 364 return nil, nil 365 } 366 tags := tagsFromDefinition(api.Metadata) 367 basePath := api.BasePath 368 if hasAbsoluteRoutes(api) { 369 basePath = "" 370 } 371 params, err := paramsFromDefinition(api.Params, basePath) 372 if err != nil { 373 return nil, err 374 } 375 var paramMap map[string]*Parameter 376 if len(params) > 0 { 377 paramMap = make(map[string]*Parameter, len(params)) 378 for _, p := range params { 379 paramMap[p.Name] = p 380 } 381 } 382 var consumes []string 383 for _, c := range api.Consumes { 384 consumes = append(consumes, c.MIMETypes...) 385 } 386 var produces []string 387 for _, p := range api.Produces { 388 produces = append(produces, p.MIMETypes...) 389 } 390 s := &Swagger{ 391 Swagger: "2.0", 392 Info: &Info{ 393 Title: api.Title, 394 Description: api.Description, 395 TermsOfService: api.TermsOfService, 396 Contact: api.Contact, 397 License: api.License, 398 Version: api.Version, 399 Extensions: extensionsFromDefinition(api.Metadata), 400 }, 401 Host: api.Host, 402 BasePath: basePath, 403 Paths: make(map[string]interface{}), 404 Schemes: api.Schemes, 405 Consumes: consumes, 406 Produces: produces, 407 Parameters: paramMap, 408 Tags: tags, 409 ExternalDocs: docsFromDefinition(api.Docs), 410 SecurityDefinitions: securityDefsFromDefinition(api.SecuritySchemes), 411 } 412 413 err = api.IterateResponses(func(r *design.ResponseDefinition) error { 414 res, err := responseSpecFromDefinition(s, api, r) 415 if err != nil { 416 return err 417 } 418 if s.Responses == nil { 419 s.Responses = make(map[string]*Response) 420 } 421 s.Responses[r.Name] = res 422 return nil 423 }) 424 if err != nil { 425 return nil, err 426 } 427 err = api.IterateResources(func(res *design.ResourceDefinition) error { 428 for k, v := range extensionsFromDefinition(res.Metadata) { 429 s.Paths[k] = v 430 } 431 err := res.IterateFileServers(func(fs *design.FileServerDefinition) error { 432 if !mustGenerate(fs.Metadata) { 433 return nil 434 } 435 return buildPathFromFileServer(s, api, fs) 436 }) 437 if err != nil { 438 return err 439 } 440 return res.IterateActions(func(a *design.ActionDefinition) error { 441 if !mustGenerate(a.Metadata) { 442 return nil 443 } 444 for _, route := range a.Routes { 445 if err := buildPathFromDefinition(s, api, route, basePath); err != nil { 446 return err 447 } 448 } 449 return nil 450 }) 451 }) 452 if err != nil { 453 return nil, err 454 } 455 if len(genschema.Definitions) > 0 { 456 s.Definitions = make(map[string]*genschema.JSONSchema) 457 for n, d := range genschema.Definitions { 458 // sad but swagger doesn't support these 459 d.Media = nil 460 d.Links = nil 461 s.Definitions[n] = d 462 } 463 } 464 return s, nil 465 } 466 467 // mustGenerate returns true if the metadata indicates that a Swagger specification should be 468 // generated, false otherwise. 469 func mustGenerate(meta dslengine.MetadataDefinition) bool { 470 if m, ok := meta["swagger:generate"]; ok { 471 if len(m) > 0 && m[0] == "false" { 472 return false 473 } 474 } 475 return true 476 } 477 478 // hasAbsoluteRoutes returns true if any action exposed by the API uses an absolute route of if the 479 // API has file servers. This is needed as Swagger does not support exceptions to the base path so 480 // if the API has any absolute route the base path must be "/" and all routes must be absolutes. 481 func hasAbsoluteRoutes(api *design.APIDefinition) bool { 482 hasAbsoluteRoutes := false 483 for _, res := range api.Resources { 484 for _, fs := range res.FileServers { 485 if !mustGenerate(fs.Metadata) { 486 continue 487 } 488 hasAbsoluteRoutes = true 489 break 490 } 491 for _, a := range res.Actions { 492 if !mustGenerate(a.Metadata) { 493 continue 494 } 495 for _, ro := range a.Routes { 496 if ro.IsAbsolute() { 497 hasAbsoluteRoutes = true 498 break 499 } 500 } 501 if hasAbsoluteRoutes { 502 break 503 } 504 } 505 if hasAbsoluteRoutes { 506 break 507 } 508 } 509 return hasAbsoluteRoutes 510 } 511 512 func securityDefsFromDefinition(schemes []*design.SecuritySchemeDefinition) map[string]*SecurityDefinition { 513 if len(schemes) == 0 { 514 return nil 515 } 516 517 defs := make(map[string]*SecurityDefinition) 518 for _, scheme := range schemes { 519 def := &SecurityDefinition{ 520 Type: scheme.Type, 521 Description: scheme.Description, 522 Name: scheme.Name, 523 In: scheme.In, 524 Flow: scheme.Flow, 525 AuthorizationURL: scheme.AuthorizationURL, 526 TokenURL: scheme.TokenURL, 527 Scopes: scheme.Scopes, 528 Extensions: extensionsFromDefinition(scheme.Metadata), 529 } 530 if scheme.Kind == design.JWTSecurityKind { 531 if def.TokenURL != "" { 532 def.Description += fmt.Sprintf("\n\n**Token URL**: %s", def.TokenURL) 533 def.TokenURL = "" 534 } 535 if len(def.Scopes) != 0 { 536 def.Description += fmt.Sprintf("\n\n**Security Scopes**:\n%s", scopesMapList(def.Scopes)) 537 def.Scopes = nil 538 } 539 } 540 defs[scheme.SchemeName] = def 541 } 542 return defs 543 } 544 545 func scopesMapList(scopes map[string]string) string { 546 names := []string{} 547 for name := range scopes { 548 names = append(names, name) 549 } 550 sort.Strings(names) 551 552 lines := []string{} 553 for _, name := range names { 554 lines = append(lines, fmt.Sprintf(" * `%s`: %s", name, scopes[name])) 555 } 556 return strings.Join(lines, "\n") 557 } 558 559 func tagsFromDefinition(mdata dslengine.MetadataDefinition) (tags []*Tag) { 560 var keys []string 561 for k := range mdata { 562 keys = append(keys, k) 563 } 564 sort.Strings(keys) 565 for _, key := range keys { 566 chunks := strings.Split(key, ":") 567 if len(chunks) != 3 { 568 continue 569 } 570 if chunks[0] != "swagger" || chunks[1] != "tag" { 571 continue 572 } 573 574 tag := &Tag{Name: chunks[2]} 575 576 mdata[key] = mdata[fmt.Sprintf("%s:desc", key)] 577 if len(mdata[key]) != 0 { 578 tag.Description = mdata[key][0] 579 } 580 581 hasDocs := false 582 docs := &ExternalDocs{} 583 584 mdata[key] = mdata[fmt.Sprintf("%s:url", key)] 585 if len(mdata[key]) != 0 { 586 docs.URL = mdata[key][0] 587 hasDocs = true 588 } 589 590 mdata[key] = mdata[fmt.Sprintf("%s:url:desc", key)] 591 if len(mdata[key]) != 0 { 592 docs.Description = mdata[key][0] 593 hasDocs = true 594 } 595 596 if hasDocs { 597 tag.ExternalDocs = docs 598 } 599 600 tag.Extensions = extensionsFromDefinition(mdata) 601 602 tags = append(tags, tag) 603 } 604 605 return 606 } 607 608 func tagNamesFromDefinitions(mdatas ...dslengine.MetadataDefinition) (tagNames []string) { 609 for _, mdata := range mdatas { 610 tags := tagsFromDefinition(mdata) 611 for _, tag := range tags { 612 tagNames = append(tagNames, tag.Name) 613 } 614 } 615 return 616 } 617 618 func summaryFromDefinition(name string, metadata dslengine.MetadataDefinition) string { 619 for n, mdata := range metadata { 620 if n == "swagger:summary" && len(mdata) > 0 { 621 return mdata[0] 622 } 623 } 624 return name 625 } 626 627 func extensionsFromDefinition(mdata dslengine.MetadataDefinition) map[string]interface{} { 628 extensions := make(map[string]interface{}) 629 for key, value := range mdata { 630 chunks := strings.Split(key, ":") 631 if len(chunks) != 3 { 632 continue 633 } 634 if chunks[0] != "swagger" || chunks[1] != "extension" { 635 continue 636 } 637 if strings.HasPrefix(chunks[2], "x-") != true { 638 continue 639 } 640 val := value[0] 641 ival := interface{}(val) 642 if err := json.Unmarshal([]byte(val), &ival); err != nil { 643 extensions[chunks[2]] = val 644 continue 645 } 646 extensions[chunks[2]] = ival 647 } 648 if len(extensions) == 0 { 649 return nil 650 } 651 return extensions 652 } 653 654 func paramsFromDefinition(params *design.AttributeDefinition, path string) ([]*Parameter, error) { 655 if params == nil { 656 return nil, nil 657 } 658 obj := params.Type.ToObject() 659 if obj == nil { 660 return nil, fmt.Errorf("invalid parameters definition, not an object") 661 } 662 res := make([]*Parameter, len(obj)) 663 i := 0 664 wildcards := design.ExtractWildcards(path) 665 obj.IterateAttributes(func(n string, at *design.AttributeDefinition) error { 666 in := "query" 667 required := params.IsRequired(n) 668 for _, w := range wildcards { 669 if n == w { 670 in = "path" 671 required = true 672 break 673 } 674 } 675 param := paramFor(at, n, in, required) 676 res[i] = param 677 i++ 678 return nil 679 }) 680 return res, nil 681 } 682 683 func paramsFromHeaders(action *design.ActionDefinition) []*Parameter { 684 params := []*Parameter{} 685 action.IterateHeaders(func(name string, required bool, header *design.AttributeDefinition) error { 686 p := paramFor(header, name, "header", required) 687 params = append(params, p) 688 return nil 689 }) 690 return params 691 } 692 693 func paramsFromPayload(payload *design.UserTypeDefinition) ([]*Parameter, error) { 694 if payload == nil { 695 return nil, nil 696 } 697 obj := payload.Type.ToObject() 698 if obj == nil { 699 return nil, fmt.Errorf("invalid parameters definition, not an object") 700 } 701 res := make([]*Parameter, len(obj)) 702 i := 0 703 obj.IterateAttributes(func(n string, at *design.AttributeDefinition) error { 704 in := "formData" 705 required := payload.IsRequired(n) 706 param := paramFor(at, n, in, required) 707 res[i] = param 708 i++ 709 return nil 710 }) 711 return res, nil 712 } 713 714 func paramFor(at *design.AttributeDefinition, name, in string, required bool) *Parameter { 715 p := &Parameter{ 716 In: in, 717 Name: name, 718 Default: toStringMap(at.DefaultValue), 719 Description: at.Description, 720 Required: required, 721 Type: at.Type.Name(), 722 } 723 if at.Type.IsArray() { 724 p.Items = itemsFromDefinition(at.Type.ToArray().ElemType) 725 p.CollectionFormat = "multi" 726 } 727 p.Extensions = extensionsFromDefinition(at.Metadata) 728 initValidations(at, p) 729 return p 730 } 731 732 // toStringMap converts map[interface{}]interface{} to a map[string]interface{} when possible. 733 func toStringMap(val interface{}) interface{} { 734 switch actual := val.(type) { 735 case map[interface{}]interface{}: 736 m := make(map[string]interface{}) 737 for k, v := range actual { 738 m[toString(k)] = toStringMap(v) 739 } 740 return m 741 case []interface{}: 742 mapSlice := make([]interface{}, len(actual)) 743 for i, e := range actual { 744 mapSlice[i] = toStringMap(e) 745 } 746 return mapSlice 747 default: 748 return actual 749 } 750 } 751 752 // toString returns the string representation of the given type. 753 func toString(val interface{}) string { 754 switch actual := val.(type) { 755 case string: 756 return actual 757 case int: 758 return strconv.Itoa(actual) 759 case float64: 760 return strconv.FormatFloat(actual, 'f', -1, 64) 761 case bool: 762 return strconv.FormatBool(actual) 763 default: 764 panic("unexpected key type") 765 } 766 } 767 768 func itemsFromDefinition(at *design.AttributeDefinition) *Items { 769 items := &Items{Type: at.Type.Name()} 770 initValidations(at, items) 771 if at.Type.IsArray() { 772 items.Items = itemsFromDefinition(at.Type.ToArray().ElemType) 773 } 774 return items 775 } 776 777 func responseSpecFromDefinition(s *Swagger, api *design.APIDefinition, r *design.ResponseDefinition) (*Response, error) { 778 var schema *genschema.JSONSchema 779 if r.MediaType != "" { 780 if mt, ok := api.MediaTypes[design.CanonicalIdentifier(r.MediaType)]; ok { 781 view := r.ViewName 782 if view == "" { 783 view = design.DefaultView 784 } 785 schema = genschema.NewJSONSchema() 786 schema.Ref = genschema.MediaTypeRef(api, mt, view) 787 } 788 } 789 headers, err := headersFromDefinition(r.Headers) 790 if err != nil { 791 return nil, err 792 } 793 return &Response{ 794 Description: r.Description, 795 Schema: schema, 796 Headers: headers, 797 Extensions: extensionsFromDefinition(r.Metadata), 798 }, nil 799 } 800 801 func responseFromDefinition(s *Swagger, api *design.APIDefinition, r *design.ResponseDefinition) (*Response, error) { 802 var ( 803 response *Response 804 err error 805 ) 806 response, err = responseSpecFromDefinition(s, api, r) 807 if err != nil { 808 return nil, err 809 } 810 if r.Standard { 811 if s.Responses == nil { 812 s.Responses = make(map[string]*Response) 813 } 814 if _, ok := s.Responses[r.Name]; !ok { 815 sp, err := responseSpecFromDefinition(s, api, r) 816 if err != nil { 817 return nil, err 818 } 819 s.Responses[r.Name] = sp 820 } 821 } 822 return response, nil 823 } 824 825 func headersFromDefinition(headers *design.AttributeDefinition) (map[string]*Header, error) { 826 if headers == nil { 827 return nil, nil 828 } 829 obj := headers.Type.ToObject() 830 if obj == nil { 831 return nil, fmt.Errorf("invalid headers definition, not an object") 832 } 833 res := make(map[string]*Header) 834 obj.IterateAttributes(func(n string, at *design.AttributeDefinition) error { 835 header := &Header{ 836 Default: at.DefaultValue, 837 Description: at.Description, 838 Type: at.Type.Name(), 839 } 840 initValidations(at, header) 841 res[n] = header 842 return nil 843 }) 844 return res, nil 845 } 846 847 func buildPathFromFileServer(s *Swagger, api *design.APIDefinition, fs *design.FileServerDefinition) error { 848 wcs := design.ExtractWildcards(fs.RequestPath) 849 var param []*Parameter 850 if len(wcs) > 0 { 851 param = []*Parameter{{ 852 In: "path", 853 Name: wcs[0], 854 Description: "Relative file path", 855 Required: true, 856 Type: "string", 857 }} 858 } 859 860 responses := map[string]*Response{ 861 "200": { 862 Description: "File downloaded", 863 Schema: &genschema.JSONSchema{Type: genschema.JSONFile}, 864 }, 865 } 866 if len(wcs) > 0 { 867 schema := genschema.TypeSchema(api, design.ErrorMedia) 868 responses["404"] = &Response{Description: "File not found", Schema: schema} 869 } 870 871 operationID := fmt.Sprintf("%s#%s", fs.Parent.Name, fs.RequestPath) 872 schemes := api.Schemes 873 874 operation := &Operation{ 875 Description: fs.Description, 876 Summary: summaryFromDefinition(fmt.Sprintf("Download %s", fs.FilePath), fs.Metadata), 877 ExternalDocs: docsFromDefinition(fs.Docs), 878 OperationID: operationID, 879 Parameters: param, 880 Responses: responses, 881 Schemes: schemes, 882 } 883 884 applySecurity(operation, fs.Security) 885 886 key := design.WildcardRegex.ReplaceAllStringFunc( 887 fs.RequestPath, 888 func(w string) string { 889 return fmt.Sprintf("/{%s}", w[2:]) 890 }, 891 ) 892 if key == "" { 893 key = "/" 894 } 895 var path interface{} 896 var ok bool 897 if path, ok = s.Paths[key]; !ok { 898 path = new(Path) 899 s.Paths[key] = path 900 } 901 p := path.(*Path) 902 p.Get = operation 903 p.Extensions = extensionsFromDefinition(fs.Metadata) 904 905 return nil 906 } 907 908 func buildPathFromDefinition(s *Swagger, api *design.APIDefinition, route *design.RouteDefinition, basePath string) error { 909 action := route.Parent 910 911 tagNames := tagNamesFromDefinitions(action.Parent.Metadata, action.Metadata) 912 if len(tagNames) == 0 { 913 // By default tag with resource name 914 tagNames = []string{route.Parent.Parent.Name} 915 } 916 params, err := paramsFromDefinition(action.AllParams(), route.FullPath()) 917 if err != nil { 918 return err 919 } 920 921 params = append(params, paramsFromHeaders(action)...) 922 923 responses := make(map[string]*Response, len(action.Responses)) 924 for _, r := range action.Responses { 925 resp, err := responseFromDefinition(s, api, r) 926 if err != nil { 927 return err 928 } 929 responses[strconv.Itoa(r.Status)] = resp 930 } 931 932 consumesMultipart := false 933 if action.Payload != nil { 934 if action.PayloadMultipart { 935 p, err := paramsFromPayload(action.Payload) 936 if err != nil { 937 return err 938 } 939 params = append(params, p...) 940 consumesMultipart = true 941 } else { 942 payloadSchema := genschema.TypeSchema(api, action.Payload) 943 pp := &Parameter{ 944 Name: "payload", 945 In: "body", 946 Description: action.Payload.Description, 947 Required: !action.PayloadOptional, 948 Schema: payloadSchema, 949 } 950 params = append(params, pp) 951 } 952 } 953 954 operationID := fmt.Sprintf("%s#%s", action.Parent.Name, action.Name) 955 index := 0 956 for i, rt := range action.Routes { 957 if rt == route { 958 index = i 959 break 960 } 961 } 962 if index > 0 { 963 operationID = fmt.Sprintf("%s#%d", operationID, index) 964 } 965 966 schemes := action.Schemes 967 if len(schemes) == 0 { 968 schemes = api.Schemes 969 } 970 971 operation := &Operation{ 972 Tags: tagNames, 973 Description: action.Description, 974 Summary: summaryFromDefinition(action.Name+" "+action.Parent.Name, action.Metadata), 975 ExternalDocs: docsFromDefinition(action.Docs), 976 OperationID: operationID, 977 Parameters: params, 978 Responses: responses, 979 Schemes: schemes, 980 Deprecated: false, 981 Extensions: extensionsFromDefinition(route.Metadata), 982 } 983 984 if consumesMultipart { 985 operation.Consumes = append(operation.Consumes, "multipart/form-data") 986 } 987 988 computeProduces(operation, s, action) 989 applySecurity(operation, action.Security) 990 991 computePaths(operation, s, route, basePath) 992 return nil 993 } 994 995 func computeProduces(operation *Operation, s *Swagger, action *design.ActionDefinition) { 996 produces := make(map[string]struct{}) 997 action.IterateResponses(func(resp *design.ResponseDefinition) error { 998 if resp.MediaType != "" { 999 produces[resp.MediaType] = struct{}{} 1000 } 1001 return nil 1002 }) 1003 subset := true 1004 for p := range produces { 1005 found := false 1006 for _, p2 := range s.Produces { 1007 if p == p2 { 1008 found = true 1009 break 1010 } 1011 } 1012 if !found { 1013 subset = false 1014 break 1015 } 1016 } 1017 if !subset { 1018 operation.Produces = make([]string, len(produces)) 1019 i := 0 1020 for p := range produces { 1021 operation.Produces[i] = p 1022 i++ 1023 } 1024 sort.Strings(operation.Produces) 1025 } 1026 } 1027 1028 func computePaths(operation *Operation, s *Swagger, route *design.RouteDefinition, basePath string) { 1029 key := design.WildcardRegex.ReplaceAllStringFunc( 1030 route.FullPath(), 1031 func(w string) string { 1032 return fmt.Sprintf("/{%s}", w[2:]) 1033 }, 1034 ) 1035 bp := design.WildcardRegex.ReplaceAllStringFunc( 1036 basePath, 1037 func(w string) string { 1038 return fmt.Sprintf("/{%s}", w[2:]) 1039 }, 1040 ) 1041 if bp != "/" { 1042 key = strings.TrimPrefix(key, bp) 1043 } 1044 if key == "" { 1045 key = "/" 1046 } 1047 var path interface{} 1048 var ok bool 1049 if path, ok = s.Paths[key]; !ok { 1050 path = new(Path) 1051 s.Paths[key] = path 1052 } 1053 p := path.(*Path) 1054 switch route.Verb { 1055 case "GET": 1056 p.Get = operation 1057 case "PUT": 1058 p.Put = operation 1059 case "POST": 1060 p.Post = operation 1061 case "DELETE": 1062 p.Delete = operation 1063 case "OPTIONS": 1064 p.Options = operation 1065 case "HEAD": 1066 p.Head = operation 1067 case "PATCH": 1068 p.Patch = operation 1069 } 1070 p.Extensions = extensionsFromDefinition(route.Parent.Metadata) 1071 } 1072 1073 func applySecurity(operation *Operation, security *design.SecurityDefinition) { 1074 if security != nil && security.Scheme.Kind != design.NoSecurityKind { 1075 if security.Scheme.Kind == design.JWTSecurityKind && len(security.Scopes) > 0 { 1076 if operation.Description != "" { 1077 operation.Description += "\n\n" 1078 } 1079 operation.Description += fmt.Sprintf("Required security scopes:\n%s", scopesList(security.Scopes)) 1080 } 1081 scopes := security.Scopes 1082 if scopes == nil { 1083 scopes = make([]string, 0) 1084 } 1085 sec := []map[string][]string{{security.Scheme.SchemeName: scopes}} 1086 operation.Security = sec 1087 } 1088 } 1089 1090 func scopesList(scopes []string) string { 1091 sort.Strings(scopes) 1092 1093 var lines []string 1094 for _, scope := range scopes { 1095 lines = append(lines, fmt.Sprintf(" * `%s`", scope)) 1096 } 1097 return strings.Join(lines, "\n") 1098 } 1099 1100 func docsFromDefinition(docs *design.DocsDefinition) *ExternalDocs { 1101 if docs == nil { 1102 return nil 1103 } 1104 return &ExternalDocs{ 1105 Description: docs.Description, 1106 URL: docs.URL, 1107 } 1108 } 1109 1110 func initEnumValidation(def interface{}, values []interface{}) { 1111 switch actual := def.(type) { 1112 case *Parameter: 1113 actual.Enum = values 1114 case *Header: 1115 actual.Enum = values 1116 case *Items: 1117 actual.Enum = values 1118 } 1119 } 1120 1121 func initFormatValidation(def interface{}, format string) { 1122 switch actual := def.(type) { 1123 case *Parameter: 1124 actual.Format = format 1125 case *Header: 1126 actual.Format = format 1127 case *Items: 1128 actual.Format = format 1129 } 1130 } 1131 1132 func initPatternValidation(def interface{}, pattern string) { 1133 switch actual := def.(type) { 1134 case *Parameter: 1135 actual.Pattern = pattern 1136 case *Header: 1137 actual.Pattern = pattern 1138 case *Items: 1139 actual.Pattern = pattern 1140 } 1141 } 1142 1143 func initMinimumValidation(def interface{}, min *float64) { 1144 switch actual := def.(type) { 1145 case *Parameter: 1146 actual.Minimum = min 1147 actual.ExclusiveMinimum = false 1148 case *Header: 1149 actual.Minimum = min 1150 actual.ExclusiveMinimum = false 1151 case *Items: 1152 actual.Minimum = min 1153 actual.ExclusiveMinimum = false 1154 } 1155 } 1156 1157 func initMaximumValidation(def interface{}, max *float64) { 1158 switch actual := def.(type) { 1159 case *Parameter: 1160 actual.Maximum = max 1161 actual.ExclusiveMaximum = false 1162 case *Header: 1163 actual.Maximum = max 1164 actual.ExclusiveMaximum = false 1165 case *Items: 1166 actual.Maximum = max 1167 actual.ExclusiveMaximum = false 1168 } 1169 } 1170 1171 func initMinLengthValidation(def interface{}, isArray bool, min *int) { 1172 switch actual := def.(type) { 1173 case *Parameter: 1174 if isArray { 1175 actual.MinItems = min 1176 } else { 1177 actual.MinLength = min 1178 } 1179 case *Header: 1180 actual.MinLength = min 1181 case *Items: 1182 actual.MinLength = min 1183 } 1184 } 1185 1186 func initMaxLengthValidation(def interface{}, isArray bool, max *int) { 1187 switch actual := def.(type) { 1188 case *Parameter: 1189 if isArray { 1190 actual.MaxItems = max 1191 } else { 1192 actual.MaxLength = max 1193 } 1194 case *Header: 1195 actual.MaxLength = max 1196 case *Items: 1197 actual.MaxLength = max 1198 } 1199 } 1200 1201 func initValidations(attr *design.AttributeDefinition, def interface{}) { 1202 val := attr.Validation 1203 if val == nil { 1204 return 1205 } 1206 initEnumValidation(def, val.Values) 1207 initFormatValidation(def, val.Format) 1208 initPatternValidation(def, val.Pattern) 1209 if val.Minimum != nil { 1210 initMinimumValidation(def, val.Minimum) 1211 } 1212 if val.Maximum != nil { 1213 initMaximumValidation(def, val.Maximum) 1214 } 1215 if val.MinLength != nil { 1216 initMinLengthValidation(def, attr.Type.IsArray(), val.MinLength) 1217 } 1218 if val.MaxLength != nil { 1219 initMaxLengthValidation(def, attr.Type.IsArray(), val.MaxLength) 1220 } 1221 }