github.com/blp1526/goa@v1.4.0/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 "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 } 726 p.Extensions = extensionsFromDefinition(at.Metadata) 727 initValidations(at, p) 728 return p 729 } 730 731 // toStringMap converts map[interface{}]interface{} to a map[string]interface{} when possible. 732 func toStringMap(val interface{}) interface{} { 733 switch actual := val.(type) { 734 case map[interface{}]interface{}: 735 m := make(map[string]interface{}) 736 for k, v := range actual { 737 m[toString(k)] = toStringMap(v) 738 } 739 return m 740 case []interface{}: 741 mapSlice := make([]interface{}, len(actual)) 742 for i, e := range actual { 743 mapSlice[i] = toStringMap(e) 744 } 745 return mapSlice 746 default: 747 return actual 748 } 749 } 750 751 // toString returns the string representation of the given type. 752 func toString(val interface{}) string { 753 switch actual := val.(type) { 754 case string: 755 return actual 756 case int: 757 return strconv.Itoa(actual) 758 case float64: 759 return strconv.FormatFloat(actual, 'f', -1, 64) 760 case bool: 761 return strconv.FormatBool(actual) 762 default: 763 panic("unexpected key type") 764 } 765 } 766 767 func itemsFromDefinition(at *design.AttributeDefinition) *Items { 768 items := &Items{Type: at.Type.Name()} 769 initValidations(at, items) 770 if at.Type.IsArray() { 771 items.Items = itemsFromDefinition(at.Type.ToArray().ElemType) 772 } 773 return items 774 } 775 776 func responseSpecFromDefinition(s *Swagger, api *design.APIDefinition, r *design.ResponseDefinition) (*Response, error) { 777 var schema *genschema.JSONSchema 778 if r.MediaType != "" { 779 if mt, ok := api.MediaTypes[design.CanonicalIdentifier(r.MediaType)]; ok { 780 view := r.ViewName 781 if view == "" { 782 view = design.DefaultView 783 } 784 schema = genschema.NewJSONSchema() 785 schema.Ref = genschema.MediaTypeRef(api, mt, view) 786 } 787 } 788 headers, err := headersFromDefinition(r.Headers) 789 if err != nil { 790 return nil, err 791 } 792 return &Response{ 793 Description: r.Description, 794 Schema: schema, 795 Headers: headers, 796 Extensions: extensionsFromDefinition(r.Metadata), 797 }, nil 798 } 799 800 func responseFromDefinition(s *Swagger, api *design.APIDefinition, r *design.ResponseDefinition) (*Response, error) { 801 var ( 802 response *Response 803 err error 804 ) 805 response, err = responseSpecFromDefinition(s, api, r) 806 if err != nil { 807 return nil, err 808 } 809 if r.Standard { 810 if s.Responses == nil { 811 s.Responses = make(map[string]*Response) 812 } 813 if _, ok := s.Responses[r.Name]; !ok { 814 sp, err := responseSpecFromDefinition(s, api, r) 815 if err != nil { 816 return nil, err 817 } 818 s.Responses[r.Name] = sp 819 } 820 } 821 return response, nil 822 } 823 824 func headersFromDefinition(headers *design.AttributeDefinition) (map[string]*Header, error) { 825 if headers == nil { 826 return nil, nil 827 } 828 obj := headers.Type.ToObject() 829 if obj == nil { 830 return nil, fmt.Errorf("invalid headers definition, not an object") 831 } 832 res := make(map[string]*Header) 833 obj.IterateAttributes(func(n string, at *design.AttributeDefinition) error { 834 header := &Header{ 835 Default: at.DefaultValue, 836 Description: at.Description, 837 Type: at.Type.Name(), 838 } 839 initValidations(at, header) 840 res[n] = header 841 return nil 842 }) 843 return res, nil 844 } 845 846 func buildPathFromFileServer(s *Swagger, api *design.APIDefinition, fs *design.FileServerDefinition) error { 847 wcs := design.ExtractWildcards(fs.RequestPath) 848 var param []*Parameter 849 if len(wcs) > 0 { 850 param = []*Parameter{{ 851 In: "path", 852 Name: wcs[0], 853 Description: "Relative file path", 854 Required: true, 855 Type: "string", 856 }} 857 } 858 859 responses := map[string]*Response{ 860 "200": { 861 Description: "File downloaded", 862 Schema: &genschema.JSONSchema{Type: genschema.JSONFile}, 863 }, 864 } 865 if len(wcs) > 0 { 866 schema := genschema.TypeSchema(api, design.ErrorMedia) 867 responses["404"] = &Response{Description: "File not found", Schema: schema} 868 } 869 870 operationID := fmt.Sprintf("%s#%s", fs.Parent.Name, fs.RequestPath) 871 schemes := api.Schemes 872 873 operation := &Operation{ 874 Description: fs.Description, 875 Summary: summaryFromDefinition(fmt.Sprintf("Download %s", fs.FilePath), fs.Metadata), 876 ExternalDocs: docsFromDefinition(fs.Docs), 877 OperationID: operationID, 878 Parameters: param, 879 Responses: responses, 880 Schemes: schemes, 881 } 882 883 applySecurity(operation, fs.Security) 884 885 key := design.WildcardRegex.ReplaceAllStringFunc( 886 fs.RequestPath, 887 func(w string) string { 888 return fmt.Sprintf("/{%s}", w[2:]) 889 }, 890 ) 891 if key == "" { 892 key = "/" 893 } 894 var path interface{} 895 var ok bool 896 if path, ok = s.Paths[key]; !ok { 897 path = new(Path) 898 s.Paths[key] = path 899 } 900 p := path.(*Path) 901 p.Get = operation 902 p.Extensions = extensionsFromDefinition(fs.Metadata) 903 904 return nil 905 } 906 907 func buildPathFromDefinition(s *Swagger, api *design.APIDefinition, route *design.RouteDefinition, basePath string) error { 908 action := route.Parent 909 910 tagNames := tagNamesFromDefinitions(action.Parent.Metadata, action.Metadata) 911 if len(tagNames) == 0 { 912 // By default tag with resource name 913 tagNames = []string{route.Parent.Parent.Name} 914 } 915 params, err := paramsFromDefinition(action.AllParams(), route.FullPath()) 916 if err != nil { 917 return err 918 } 919 920 params = append(params, paramsFromHeaders(action)...) 921 922 responses := make(map[string]*Response, len(action.Responses)) 923 for _, r := range action.Responses { 924 resp, err := responseFromDefinition(s, api, r) 925 if err != nil { 926 return err 927 } 928 responses[strconv.Itoa(r.Status)] = resp 929 } 930 931 consumesMultipart := false 932 if action.Payload != nil { 933 if action.PayloadMultipart { 934 p, err := paramsFromPayload(action.Payload) 935 if err != nil { 936 return err 937 } 938 params = append(params, p...) 939 consumesMultipart = true 940 } else { 941 payloadSchema := genschema.TypeSchema(api, action.Payload) 942 pp := &Parameter{ 943 Name: "payload", 944 In: "body", 945 Description: action.Payload.Description, 946 Required: !action.PayloadOptional, 947 Schema: payloadSchema, 948 } 949 params = append(params, pp) 950 } 951 } 952 953 operationID := fmt.Sprintf("%s#%s", action.Parent.Name, action.Name) 954 index := 0 955 for i, rt := range action.Routes { 956 if rt == route { 957 index = i 958 break 959 } 960 } 961 if index > 0 { 962 operationID = fmt.Sprintf("%s#%d", operationID, index) 963 } 964 965 schemes := action.Schemes 966 if len(schemes) == 0 { 967 schemes = api.Schemes 968 } 969 970 operation := &Operation{ 971 Tags: tagNames, 972 Description: action.Description, 973 Summary: summaryFromDefinition(action.Name+" "+action.Parent.Name, action.Metadata), 974 ExternalDocs: docsFromDefinition(action.Docs), 975 OperationID: operationID, 976 Parameters: params, 977 Responses: responses, 978 Schemes: schemes, 979 Deprecated: false, 980 Extensions: extensionsFromDefinition(route.Metadata), 981 } 982 983 if consumesMultipart { 984 operation.Consumes = append(operation.Consumes, "multipart/form-data") 985 } 986 987 computeProduces(operation, s, action) 988 applySecurity(operation, action.Security) 989 990 computePaths(operation, s, route, basePath) 991 return nil 992 } 993 994 func computeProduces(operation *Operation, s *Swagger, action *design.ActionDefinition) { 995 produces := make(map[string]struct{}) 996 action.IterateResponses(func(resp *design.ResponseDefinition) error { 997 if resp.MediaType != "" { 998 produces[resp.MediaType] = struct{}{} 999 } 1000 return nil 1001 }) 1002 subset := true 1003 for p := range produces { 1004 found := false 1005 for _, p2 := range s.Produces { 1006 if p == p2 { 1007 found = true 1008 break 1009 } 1010 } 1011 if !found { 1012 subset = false 1013 break 1014 } 1015 } 1016 if !subset { 1017 operation.Produces = make([]string, len(produces)) 1018 i := 0 1019 for p := range produces { 1020 operation.Produces[i] = p 1021 i++ 1022 } 1023 sort.Strings(operation.Produces) 1024 } 1025 } 1026 1027 func computePaths(operation *Operation, s *Swagger, route *design.RouteDefinition, basePath string) { 1028 key := design.WildcardRegex.ReplaceAllStringFunc( 1029 route.FullPath(), 1030 func(w string) string { 1031 return fmt.Sprintf("/{%s}", w[2:]) 1032 }, 1033 ) 1034 bp := design.WildcardRegex.ReplaceAllStringFunc( 1035 basePath, 1036 func(w string) string { 1037 return fmt.Sprintf("/{%s}", w[2:]) 1038 }, 1039 ) 1040 if bp != "/" { 1041 key = strings.TrimPrefix(key, bp) 1042 } 1043 if key == "" { 1044 key = "/" 1045 } 1046 var path interface{} 1047 var ok bool 1048 if path, ok = s.Paths[key]; !ok { 1049 path = new(Path) 1050 s.Paths[key] = path 1051 } 1052 p := path.(*Path) 1053 switch route.Verb { 1054 case "GET": 1055 p.Get = operation 1056 case "PUT": 1057 p.Put = operation 1058 case "POST": 1059 p.Post = operation 1060 case "DELETE": 1061 p.Delete = operation 1062 case "OPTIONS": 1063 p.Options = operation 1064 case "HEAD": 1065 p.Head = operation 1066 case "PATCH": 1067 p.Patch = operation 1068 } 1069 p.Extensions = extensionsFromDefinition(route.Parent.Metadata) 1070 } 1071 1072 func applySecurity(operation *Operation, security *design.SecurityDefinition) { 1073 if security != nil && security.Scheme.Kind != design.NoSecurityKind { 1074 if security.Scheme.Kind == design.JWTSecurityKind && len(security.Scopes) > 0 { 1075 if operation.Description != "" { 1076 operation.Description += "\n\n" 1077 } 1078 operation.Description += fmt.Sprintf("Required security scopes:\n%s", scopesList(security.Scopes)) 1079 } 1080 scopes := security.Scopes 1081 if scopes == nil { 1082 scopes = make([]string, 0) 1083 } 1084 sec := []map[string][]string{{security.Scheme.SchemeName: scopes}} 1085 operation.Security = sec 1086 } 1087 } 1088 1089 func scopesList(scopes []string) string { 1090 sort.Strings(scopes) 1091 1092 var lines []string 1093 for _, scope := range scopes { 1094 lines = append(lines, fmt.Sprintf(" * `%s`", scope)) 1095 } 1096 return strings.Join(lines, "\n") 1097 } 1098 1099 func docsFromDefinition(docs *design.DocsDefinition) *ExternalDocs { 1100 if docs == nil { 1101 return nil 1102 } 1103 return &ExternalDocs{ 1104 Description: docs.Description, 1105 URL: docs.URL, 1106 } 1107 } 1108 1109 func initEnumValidation(def interface{}, values []interface{}) { 1110 switch actual := def.(type) { 1111 case *Parameter: 1112 actual.Enum = values 1113 case *Header: 1114 actual.Enum = values 1115 case *Items: 1116 actual.Enum = values 1117 } 1118 } 1119 1120 func initFormatValidation(def interface{}, format string) { 1121 switch actual := def.(type) { 1122 case *Parameter: 1123 actual.Format = format 1124 case *Header: 1125 actual.Format = format 1126 case *Items: 1127 actual.Format = format 1128 } 1129 } 1130 1131 func initPatternValidation(def interface{}, pattern string) { 1132 switch actual := def.(type) { 1133 case *Parameter: 1134 actual.Pattern = pattern 1135 case *Header: 1136 actual.Pattern = pattern 1137 case *Items: 1138 actual.Pattern = pattern 1139 } 1140 } 1141 1142 func initMinimumValidation(def interface{}, min *float64) { 1143 switch actual := def.(type) { 1144 case *Parameter: 1145 actual.Minimum = min 1146 actual.ExclusiveMinimum = false 1147 case *Header: 1148 actual.Minimum = min 1149 actual.ExclusiveMinimum = false 1150 case *Items: 1151 actual.Minimum = min 1152 actual.ExclusiveMinimum = false 1153 } 1154 } 1155 1156 func initMaximumValidation(def interface{}, max *float64) { 1157 switch actual := def.(type) { 1158 case *Parameter: 1159 actual.Maximum = max 1160 actual.ExclusiveMaximum = false 1161 case *Header: 1162 actual.Maximum = max 1163 actual.ExclusiveMaximum = false 1164 case *Items: 1165 actual.Maximum = max 1166 actual.ExclusiveMaximum = false 1167 } 1168 } 1169 1170 func initMinLengthValidation(def interface{}, isArray bool, min *int) { 1171 switch actual := def.(type) { 1172 case *Parameter: 1173 if isArray { 1174 actual.MinItems = min 1175 } else { 1176 actual.MinLength = min 1177 } 1178 case *Header: 1179 actual.MinLength = min 1180 case *Items: 1181 actual.MinLength = min 1182 } 1183 } 1184 1185 func initMaxLengthValidation(def interface{}, isArray bool, max *int) { 1186 switch actual := def.(type) { 1187 case *Parameter: 1188 if isArray { 1189 actual.MaxItems = max 1190 } else { 1191 actual.MaxLength = max 1192 } 1193 case *Header: 1194 actual.MaxLength = max 1195 case *Items: 1196 actual.MaxLength = max 1197 } 1198 } 1199 1200 func initValidations(attr *design.AttributeDefinition, def interface{}) { 1201 val := attr.Validation 1202 if val == nil { 1203 return 1204 } 1205 initEnumValidation(def, val.Values) 1206 initFormatValidation(def, val.Format) 1207 initPatternValidation(def, val.Pattern) 1208 if val.Minimum != nil { 1209 initMinimumValidation(def, val.Minimum) 1210 } 1211 if val.Maximum != nil { 1212 initMaximumValidation(def, val.Maximum) 1213 } 1214 if val.MinLength != nil { 1215 initMinLengthValidation(def, attr.Type.IsArray(), val.MinLength) 1216 } 1217 if val.MaxLength != nil { 1218 initMaxLengthValidation(def, attr.Type.IsArray(), val.MaxLength) 1219 } 1220 }