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