1// Copyright 2019 DeepMap, Inc. 2// 3// Licensed under the Apache License, Version 2.0 (the "License"); 4// you may not use this file except in compliance with the License. 5// You may obtain a copy of the License at 6// 7// http://www.apache.org/licenses/LICENSE-2.0 8// 9// Unless required by applicable law or agreed to in writing, software 10// distributed under the License is distributed on an "AS IS" BASIS, 11// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12// See the License for the specific language governing permissions and 13// limitations under the License. 14package codegen 15 16import ( 17 "fmt" 18 "net/url" 19 "regexp" 20 "sort" 21 "strconv" 22 "strings" 23 "unicode" 24 25 "github.com/getkin/kin-openapi/openapi3" 26) 27 28var pathParamRE *regexp.Regexp 29 30func init() { 31 pathParamRE = regexp.MustCompile("{[.;?]?([^{}*]+)\\*?}") 32} 33 34// Uppercase the first character in a string. This assumes UTF-8, so we have 35// to be careful with unicode, don't treat it as a byte array. 36func UppercaseFirstCharacter(str string) string { 37 if str == "" { 38 return "" 39 } 40 runes := []rune(str) 41 runes[0] = unicode.ToUpper(runes[0]) 42 return string(runes) 43} 44 45// Same as above, except lower case 46func LowercaseFirstCharacter(str string) string { 47 if str == "" { 48 return "" 49 } 50 runes := []rune(str) 51 runes[0] = unicode.ToLower(runes[0]) 52 return string(runes) 53} 54 55// This function will convert query-arg style strings to CamelCase. We will 56// use `., -, +, :, ;, _, ~, ' ', (, ), {, }, [, ]` as valid delimiters for words. 57// So, "word.word-word+word:word;word_word~word word(word)word{word}[word]" 58// would be converted to WordWordWordWordWordWordWordWordWordWordWordWordWord 59func ToCamelCase(str string) string { 60 separators := "-#@!$&=.+:;_~ (){}[]" 61 s := strings.Trim(str, " ") 62 63 n := "" 64 capNext := true 65 for _, v := range s { 66 if unicode.IsUpper(v) { 67 n += string(v) 68 } 69 if unicode.IsDigit(v) { 70 n += string(v) 71 } 72 if unicode.IsLower(v) { 73 if capNext { 74 n += strings.ToUpper(string(v)) 75 } else { 76 n += string(v) 77 } 78 } 79 80 if strings.ContainsRune(separators, v) { 81 capNext = true 82 } else { 83 capNext = false 84 } 85 } 86 return n 87} 88 89// This function returns the keys of the given SchemaRef dictionary in sorted 90// order, since Golang scrambles dictionary keys 91func SortedSchemaKeys(dict map[string]*openapi3.SchemaRef) []string { 92 keys := make([]string, len(dict)) 93 i := 0 94 for key := range dict { 95 keys[i] = key 96 i++ 97 } 98 sort.Strings(keys) 99 return keys 100} 101 102// This function is the same as above, except it sorts the keys for a Paths 103// dictionary. 104func SortedPathsKeys(dict openapi3.Paths) []string { 105 keys := make([]string, len(dict)) 106 i := 0 107 for key := range dict { 108 keys[i] = key 109 i++ 110 } 111 sort.Strings(keys) 112 return keys 113} 114 115// This function returns Operation dictionary keys in sorted order 116func SortedOperationsKeys(dict map[string]*openapi3.Operation) []string { 117 keys := make([]string, len(dict)) 118 i := 0 119 for key := range dict { 120 keys[i] = key 121 i++ 122 } 123 sort.Strings(keys) 124 return keys 125} 126 127// This function returns Responses dictionary keys in sorted order 128func SortedResponsesKeys(dict openapi3.Responses) []string { 129 keys := make([]string, len(dict)) 130 i := 0 131 for key := range dict { 132 keys[i] = key 133 i++ 134 } 135 sort.Strings(keys) 136 return keys 137} 138 139// This returns Content dictionary keys in sorted order 140func SortedContentKeys(dict openapi3.Content) []string { 141 keys := make([]string, len(dict)) 142 i := 0 143 for key := range dict { 144 keys[i] = key 145 i++ 146 } 147 sort.Strings(keys) 148 return keys 149} 150 151// This returns string map keys in sorted order 152func SortedStringKeys(dict map[string]string) []string { 153 keys := make([]string, len(dict)) 154 i := 0 155 for key := range dict { 156 keys[i] = key 157 i++ 158 } 159 sort.Strings(keys) 160 return keys 161} 162 163// This returns sorted keys for a ParameterRef dict 164func SortedParameterKeys(dict map[string]*openapi3.ParameterRef) []string { 165 keys := make([]string, len(dict)) 166 i := 0 167 for key := range dict { 168 keys[i] = key 169 i++ 170 } 171 sort.Strings(keys) 172 return keys 173} 174 175func SortedRequestBodyKeys(dict map[string]*openapi3.RequestBodyRef) []string { 176 keys := make([]string, len(dict)) 177 i := 0 178 for key := range dict { 179 keys[i] = key 180 i++ 181 } 182 sort.Strings(keys) 183 return keys 184} 185 186func SortedSecurityRequirementKeys(sr openapi3.SecurityRequirement) []string { 187 keys := make([]string, len(sr)) 188 i := 0 189 for key := range sr { 190 keys[i] = key 191 i++ 192 } 193 sort.Strings(keys) 194 return keys 195} 196 197// This function checks whether the specified string is present in an array 198// of strings 199func StringInArray(str string, array []string) bool { 200 for _, elt := range array { 201 if elt == str { 202 return true 203 } 204 } 205 return false 206} 207 208// This function takes a $ref value and converts it to a Go typename. 209// #/components/schemas/Foo -> Foo 210// #/components/parameters/Bar -> Bar 211// #/components/responses/Baz -> Baz 212// Remote components (document.json#/Foo) are supported if they present in --import-mapping 213// URL components (http://deepmap.com/schemas/document.json#/Foo) are supported if they present in --import-mapping 214// Remote and URL also support standard local paths even though the spec doesn't mention them. 215func RefPathToGoType(refPath string) (string, error) { 216 return refPathToGoType(refPath, true) 217} 218 219// refPathToGoType returns the Go typename for refPath given its 220func refPathToGoType(refPath string, local bool) (string, error) { 221 if refPath[0] == '#' { 222 pathParts := strings.Split(refPath, "/") 223 depth := len(pathParts) 224 if local { 225 if depth != 4 { 226 return "", fmt.Errorf("unexpected reference depth: %d for ref: %s local: %t", depth, refPath, local) 227 } 228 } else if depth != 4 && depth != 2 { 229 return "", fmt.Errorf("unexpected reference depth: %d for ref: %s local: %t", depth, refPath, local) 230 } 231 return SchemaNameToTypeName(pathParts[len(pathParts)-1]), nil 232 } 233 pathParts := strings.Split(refPath, "#") 234 if len(pathParts) != 2 { 235 return "", fmt.Errorf("unsupported reference: %s", refPath) 236 } 237 remoteComponent, flatComponent := pathParts[0], pathParts[1] 238 if goImport, ok := importMapping[remoteComponent]; !ok { 239 return "", fmt.Errorf("unrecognized external reference '%s'; please provide the known import for this reference using option --import-mapping", remoteComponent) 240 } else { 241 goType, err := refPathToGoType("#"+flatComponent, false) 242 if err != nil { 243 return "", err 244 } 245 return fmt.Sprintf("%s.%s", goImport.Name, goType), nil 246 } 247} 248 249// This function takes a $ref value and checks if it has link to go type. 250// #/components/schemas/Foo -> true 251// ./local/file.yml#/components/parameters/Bar -> true 252// ./local/file.yml -> false 253// The function can be used to check whether RefPathToGoType($ref) is possible. 254// 255func IsGoTypeReference(ref string) bool { 256 return ref != "" && !IsWholeDocumentReference(ref) 257} 258 259// This function takes a $ref value and checks if it is whole document reference. 260// #/components/schemas/Foo -> false 261// ./local/file.yml#/components/parameters/Bar -> false 262// ./local/file.yml -> true 263// http://deepmap.com/schemas/document.json -> true 264// http://deepmap.com/schemas/document.json#/Foo -> false 265// 266func IsWholeDocumentReference(ref string) bool { 267 return ref != "" && !strings.ContainsAny(ref, "#") 268} 269 270// This function converts a swagger style path URI with parameters to a 271// Echo compatible path URI. We need to replace all of Swagger parameters with 272// ":param". Valid input parameters are: 273// {param} 274// {param*} 275// {.param} 276// {.param*} 277// {;param} 278// {;param*} 279// {?param} 280// {?param*} 281func SwaggerUriToEchoUri(uri string) string { 282 return pathParamRE.ReplaceAllString(uri, ":$1") 283} 284 285// This function converts a swagger style path URI with parameters to a 286// Chi compatible path URI. We need to replace all of Swagger parameters with 287// "{param}". Valid input parameters are: 288// {param} 289// {param*} 290// {.param} 291// {.param*} 292// {;param} 293// {;param*} 294// {?param} 295// {?param*} 296func SwaggerUriToChiUri(uri string) string { 297 return pathParamRE.ReplaceAllString(uri, "{$1}") 298} 299 300// Returns the argument names, in order, in a given URI string, so for 301// /path/{param1}/{.param2*}/{?param3}, it would return param1, param2, param3 302func OrderedParamsFromUri(uri string) []string { 303 matches := pathParamRE.FindAllStringSubmatch(uri, -1) 304 result := make([]string, len(matches)) 305 for i, m := range matches { 306 result[i] = m[1] 307 } 308 return result 309} 310 311// Replaces path parameters of the form {param} with %s 312func ReplacePathParamsWithStr(uri string) string { 313 return pathParamRE.ReplaceAllString(uri, "%s") 314} 315 316// Reorders the given parameter definitions to match those in the path URI. 317func SortParamsByPath(path string, in []ParameterDefinition) ([]ParameterDefinition, error) { 318 pathParams := OrderedParamsFromUri(path) 319 n := len(in) 320 if len(pathParams) != n { 321 return nil, fmt.Errorf("path '%s' has %d positional parameters, but spec has %d declared", 322 path, len(pathParams), n) 323 } 324 out := make([]ParameterDefinition, len(in)) 325 for i, name := range pathParams { 326 p := ParameterDefinitions(in).FindByName(name) 327 if p == nil { 328 return nil, fmt.Errorf("path '%s' refers to parameter '%s', which doesn't exist in specification", 329 path, name) 330 } 331 out[i] = *p 332 } 333 return out, nil 334} 335 336// Returns whether the given string is a go keyword 337func IsGoKeyword(str string) bool { 338 keywords := []string{ 339 "break", 340 "case", 341 "chan", 342 "const", 343 "continue", 344 "default", 345 "defer", 346 "else", 347 "fallthrough", 348 "for", 349 "func", 350 "go", 351 "goto", 352 "if", 353 "import", 354 "interface", 355 "map", 356 "package", 357 "range", 358 "return", 359 "select", 360 "struct", 361 "switch", 362 "type", 363 "var", 364 } 365 366 for _, k := range keywords { 367 if k == str { 368 return true 369 } 370 } 371 return false 372} 373 374// IsPredeclaredGoIdentifier returns whether the given string 375// is a predefined go indentifier. 376// 377// See https://golang.org/ref/spec#Predeclared_identifiers 378func IsPredeclaredGoIdentifier(str string) bool { 379 predeclaredIdentifiers := []string{ 380 // Types 381 "bool", 382 "byte", 383 "complex64", 384 "complex128", 385 "error", 386 "float32", 387 "float64", 388 "int", 389 "int8", 390 "int16", 391 "int32", 392 "int64", 393 "rune", 394 "string", 395 "uint", 396 "uint8", 397 "uint16", 398 "uint32", 399 "uint64", 400 "uintptr", 401 // Constants 402 "true", 403 "false", 404 "iota", 405 // Zero value 406 "nil", 407 // Functions 408 "append", 409 "cap", 410 "close", 411 "complex", 412 "copy", 413 "delete", 414 "imag", 415 "len", 416 "make", 417 "new", 418 "panic", 419 "print", 420 "println", 421 "real", 422 "recover", 423 } 424 425 for _, k := range predeclaredIdentifiers { 426 if k == str { 427 return true 428 } 429 } 430 431 return false 432} 433 434// IsGoIdentity checks if the given string can be used as an identity 435// in the generated code like a type name or constant name. 436// 437// See https://golang.org/ref/spec#Identifiers 438func IsGoIdentity(str string) bool { 439 for i, c := range str { 440 if !isValidRuneForGoID(i, c) { 441 return false 442 } 443 } 444 445 return IsGoKeyword(str) 446} 447 448func isValidRuneForGoID(index int, char rune) bool { 449 if index == 0 && unicode.IsNumber(char) { 450 return false 451 } 452 453 return unicode.IsLetter(char) || char == '_' || unicode.IsNumber(char) 454} 455 456// IsValidGoIdentity checks if the given string can be used as a 457// name of variable, constant, or type. 458func IsValidGoIdentity(str string) bool { 459 if IsGoIdentity(str) { 460 return false 461 } 462 463 return !IsPredeclaredGoIdentifier(str) 464} 465 466// SanitizeGoIdentity deletes and replaces the illegal runes in the given 467// string to use the string as a valid identity. 468func SanitizeGoIdentity(str string) string { 469 sanitized := []rune(str) 470 471 for i, c := range sanitized { 472 if !isValidRuneForGoID(i, c) { 473 sanitized[i] = '_' 474 } else { 475 sanitized[i] = c 476 } 477 } 478 479 str = string(sanitized) 480 481 if IsGoKeyword(str) || IsPredeclaredGoIdentifier(str) { 482 str = "_" + str 483 } 484 485 if !IsValidGoIdentity(str) { 486 panic("here is a bug") 487 } 488 489 return str 490} 491 492// SanitizeEnumNames fixes illegal chars in the enum names 493// and removes duplicates 494func SanitizeEnumNames(enumNames []string) map[string]string { 495 dupCheck := make(map[string]int, len(enumNames)) 496 deDup := make([]string, 0, len(enumNames)) 497 498 for _, n := range enumNames { 499 if _, dup := dupCheck[n]; !dup { 500 deDup = append(deDup, n) 501 } 502 dupCheck[n] = 0 503 } 504 505 dupCheck = make(map[string]int, len(deDup)) 506 sanitizedDeDup := make(map[string]string, len(deDup)) 507 508 for _, n := range deDup { 509 sanitized := SanitizeGoIdentity(SchemaNameToTypeName(n)) 510 511 if _, dup := dupCheck[sanitized]; !dup { 512 sanitizedDeDup[sanitized] = n 513 } else { 514 sanitizedDeDup[sanitized+strconv.Itoa(dupCheck[sanitized])] = n 515 } 516 dupCheck[sanitized]++ 517 } 518 519 return sanitizedDeDup 520} 521 522// Converts a Schema name to a valid Go type name. It converts to camel case, and makes sure the name is 523// valid in Go 524func SchemaNameToTypeName(name string) string { 525 if name == "$" { 526 name = "DollarSign" 527 } else { 528 name = ToCamelCase(name) 529 // Prepend "N" to schemas starting with a number 530 if name != "" && unicode.IsDigit([]rune(name)[0]) { 531 name = "N" + name 532 } 533 } 534 return name 535} 536 537// According to the spec, additionalProperties may be true, false, or a 538// schema. If not present, true is implied. If it's a schema, true is implied. 539// If it's false, no additional properties are allowed. We're going to act a little 540// differently, in that if you want additionalProperties code to be generated, 541// you must specify an additionalProperties type 542// If additionalProperties it true/false, this field will be non-nil. 543func SchemaHasAdditionalProperties(schema *openapi3.Schema) bool { 544 if schema.AdditionalPropertiesAllowed != nil && *schema.AdditionalPropertiesAllowed { 545 return true 546 } 547 548 if schema.AdditionalProperties != nil { 549 return true 550 } 551 return false 552} 553 554// This converts a path, like Object/field1/nestedField into a go 555// type name. 556func PathToTypeName(path []string) string { 557 for i, p := range path { 558 path[i] = ToCamelCase(p) 559 } 560 return strings.Join(path, "_") 561} 562 563// StringToGoComment renders a possible multi-line string as a valid Go-Comment. 564// Each line is prefixed as a comment. 565func StringToGoComment(in string) string { 566 if len(in) == 0 || len(strings.TrimSpace(in)) == 0 { // ignore empty comment 567 return "" 568 } 569 570 // Normalize newlines from Windows/Mac to Linux 571 in = strings.Replace(in, "\r\n", "\n", -1) 572 in = strings.Replace(in, "\r", "\n", -1) 573 574 // Add comment to each line 575 var lines []string 576 for _, line := range strings.Split(in, "\n") { 577 lines = append(lines, fmt.Sprintf("// %s", line)) 578 } 579 in = strings.Join(lines, "\n") 580 581 // in case we have a multiline string which ends with \n, we would generate 582 // empty-line-comments, like `// `. Therefore remove this line comment. 583 in = strings.TrimSuffix(in, "\n// ") 584 return in 585} 586 587// This function breaks apart a path, and looks at each element. If it's 588// not a path parameter, eg, {param}, it will URL-escape the element. 589func EscapePathElements(path string) string { 590 elems := strings.Split(path, "/") 591 for i, e := range elems { 592 if strings.HasPrefix(e, "{") && strings.HasSuffix(e, "}") { 593 // This is a path parameter, we don't want to mess with its value 594 continue 595 } 596 elems[i] = url.QueryEscape(e) 597 } 598 return strings.Join(elems, "/") 599} 600