// Copyright 2019 DeepMap, Inc. // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. package codegen import ( "fmt" "net/url" "regexp" "sort" "strconv" "strings" "unicode" "github.com/getkin/kin-openapi/openapi3" ) var pathParamRE *regexp.Regexp func init() { pathParamRE = regexp.MustCompile("{[.;?]?([^{}*]+)\\*?}") } // Uppercase the first character in a string. This assumes UTF-8, so we have // to be careful with unicode, don't treat it as a byte array. func UppercaseFirstCharacter(str string) string { if str == "" { return "" } runes := []rune(str) runes[0] = unicode.ToUpper(runes[0]) return string(runes) } // Same as above, except lower case func LowercaseFirstCharacter(str string) string { if str == "" { return "" } runes := []rune(str) runes[0] = unicode.ToLower(runes[0]) return string(runes) } // This function will convert query-arg style strings to CamelCase. We will // use `., -, +, :, ;, _, ~, ' ', (, ), {, }, [, ]` as valid delimiters for words. // So, "word.word-word+word:word;word_word~word word(word)word{word}[word]" // would be converted to WordWordWordWordWordWordWordWordWordWordWordWordWord func ToCamelCase(str string) string { separators := "-#@!$&=.+:;_~ (){}[]" s := strings.Trim(str, " ") n := "" capNext := true for _, v := range s { if unicode.IsUpper(v) { n += string(v) } if unicode.IsDigit(v) { n += string(v) } if unicode.IsLower(v) { if capNext { n += strings.ToUpper(string(v)) } else { n += string(v) } } if strings.ContainsRune(separators, v) { capNext = true } else { capNext = false } } return n } // This function returns the keys of the given SchemaRef dictionary in sorted // order, since Golang scrambles dictionary keys func SortedSchemaKeys(dict map[string]*openapi3.SchemaRef) []string { keys := make([]string, len(dict)) i := 0 for key := range dict { keys[i] = key i++ } sort.Strings(keys) return keys } // This function is the same as above, except it sorts the keys for a Paths // dictionary. func SortedPathsKeys(dict openapi3.Paths) []string { keys := make([]string, len(dict)) i := 0 for key := range dict { keys[i] = key i++ } sort.Strings(keys) return keys } // This function returns Operation dictionary keys in sorted order func SortedOperationsKeys(dict map[string]*openapi3.Operation) []string { keys := make([]string, len(dict)) i := 0 for key := range dict { keys[i] = key i++ } sort.Strings(keys) return keys } // This function returns Responses dictionary keys in sorted order func SortedResponsesKeys(dict openapi3.Responses) []string { keys := make([]string, len(dict)) i := 0 for key := range dict { keys[i] = key i++ } sort.Strings(keys) return keys } // This returns Content dictionary keys in sorted order func SortedContentKeys(dict openapi3.Content) []string { keys := make([]string, len(dict)) i := 0 for key := range dict { keys[i] = key i++ } sort.Strings(keys) return keys } // This returns string map keys in sorted order func SortedStringKeys(dict map[string]string) []string { keys := make([]string, len(dict)) i := 0 for key := range dict { keys[i] = key i++ } sort.Strings(keys) return keys } // This returns sorted keys for a ParameterRef dict func SortedParameterKeys(dict map[string]*openapi3.ParameterRef) []string { keys := make([]string, len(dict)) i := 0 for key := range dict { keys[i] = key i++ } sort.Strings(keys) return keys } func SortedRequestBodyKeys(dict map[string]*openapi3.RequestBodyRef) []string { keys := make([]string, len(dict)) i := 0 for key := range dict { keys[i] = key i++ } sort.Strings(keys) return keys } func SortedSecurityRequirementKeys(sr openapi3.SecurityRequirement) []string { keys := make([]string, len(sr)) i := 0 for key := range sr { keys[i] = key i++ } sort.Strings(keys) return keys } // This function checks whether the specified string is present in an array // of strings func StringInArray(str string, array []string) bool { for _, elt := range array { if elt == str { return true } } return false } // This function takes a $ref value and converts it to a Go typename. // #/components/schemas/Foo -> Foo // #/components/parameters/Bar -> Bar // #/components/responses/Baz -> Baz // Remote components (document.json#/Foo) are supported if they present in --import-mapping // URL components (http://deepmap.com/schemas/document.json#/Foo) are supported if they present in --import-mapping // Remote and URL also support standard local paths even though the spec doesn't mention them. func RefPathToGoType(refPath string) (string, error) { return refPathToGoType(refPath, true) } // refPathToGoType returns the Go typename for refPath given its func refPathToGoType(refPath string, local bool) (string, error) { if refPath[0] == '#' { pathParts := strings.Split(refPath, "/") depth := len(pathParts) if local { if depth != 4 { return "", fmt.Errorf("unexpected reference depth: %d for ref: %s local: %t", depth, refPath, local) } } else if depth != 4 && depth != 2 { return "", fmt.Errorf("unexpected reference depth: %d for ref: %s local: %t", depth, refPath, local) } return SchemaNameToTypeName(pathParts[len(pathParts)-1]), nil } pathParts := strings.Split(refPath, "#") if len(pathParts) != 2 { return "", fmt.Errorf("unsupported reference: %s", refPath) } remoteComponent, flatComponent := pathParts[0], pathParts[1] if goImport, ok := importMapping[remoteComponent]; !ok { return "", fmt.Errorf("unrecognized external reference '%s'; please provide the known import for this reference using option --import-mapping", remoteComponent) } else { goType, err := refPathToGoType("#"+flatComponent, false) if err != nil { return "", err } return fmt.Sprintf("%s.%s", goImport.Name, goType), nil } } // This function takes a $ref value and checks if it has link to go type. // #/components/schemas/Foo -> true // ./local/file.yml#/components/parameters/Bar -> true // ./local/file.yml -> false // The function can be used to check whether RefPathToGoType($ref) is possible. // func IsGoTypeReference(ref string) bool { return ref != "" && !IsWholeDocumentReference(ref) } // This function takes a $ref value and checks if it is whole document reference. // #/components/schemas/Foo -> false // ./local/file.yml#/components/parameters/Bar -> false // ./local/file.yml -> true // http://deepmap.com/schemas/document.json -> true // http://deepmap.com/schemas/document.json#/Foo -> false // func IsWholeDocumentReference(ref string) bool { return ref != "" && !strings.ContainsAny(ref, "#") } // This function converts a swagger style path URI with parameters to a // Echo compatible path URI. We need to replace all of Swagger parameters with // ":param". Valid input parameters are: // {param} // {param*} // {.param} // {.param*} // {;param} // {;param*} // {?param} // {?param*} func SwaggerUriToEchoUri(uri string) string { return pathParamRE.ReplaceAllString(uri, ":$1") } // This function converts a swagger style path URI with parameters to a // Chi compatible path URI. We need to replace all of Swagger parameters with // "{param}". Valid input parameters are: // {param} // {param*} // {.param} // {.param*} // {;param} // {;param*} // {?param} // {?param*} func SwaggerUriToChiUri(uri string) string { return pathParamRE.ReplaceAllString(uri, "{$1}") } // Returns the argument names, in order, in a given URI string, so for // /path/{param1}/{.param2*}/{?param3}, it would return param1, param2, param3 func OrderedParamsFromUri(uri string) []string { matches := pathParamRE.FindAllStringSubmatch(uri, -1) result := make([]string, len(matches)) for i, m := range matches { result[i] = m[1] } return result } // Replaces path parameters of the form {param} with %s func ReplacePathParamsWithStr(uri string) string { return pathParamRE.ReplaceAllString(uri, "%s") } // Reorders the given parameter definitions to match those in the path URI. func SortParamsByPath(path string, in []ParameterDefinition) ([]ParameterDefinition, error) { pathParams := OrderedParamsFromUri(path) n := len(in) if len(pathParams) != n { return nil, fmt.Errorf("path '%s' has %d positional parameters, but spec has %d declared", path, len(pathParams), n) } out := make([]ParameterDefinition, len(in)) for i, name := range pathParams { p := ParameterDefinitions(in).FindByName(name) if p == nil { return nil, fmt.Errorf("path '%s' refers to parameter '%s', which doesn't exist in specification", path, name) } out[i] = *p } return out, nil } // Returns whether the given string is a go keyword func IsGoKeyword(str string) bool { keywords := []string{ "break", "case", "chan", "const", "continue", "default", "defer", "else", "fallthrough", "for", "func", "go", "goto", "if", "import", "interface", "map", "package", "range", "return", "select", "struct", "switch", "type", "var", } for _, k := range keywords { if k == str { return true } } return false } // IsPredeclaredGoIdentifier returns whether the given string // is a predefined go indentifier. // // See https://golang.org/ref/spec#Predeclared_identifiers func IsPredeclaredGoIdentifier(str string) bool { predeclaredIdentifiers := []string{ // Types "bool", "byte", "complex64", "complex128", "error", "float32", "float64", "int", "int8", "int16", "int32", "int64", "rune", "string", "uint", "uint8", "uint16", "uint32", "uint64", "uintptr", // Constants "true", "false", "iota", // Zero value "nil", // Functions "append", "cap", "close", "complex", "copy", "delete", "imag", "len", "make", "new", "panic", "print", "println", "real", "recover", } for _, k := range predeclaredIdentifiers { if k == str { return true } } return false } // IsGoIdentity checks if the given string can be used as an identity // in the generated code like a type name or constant name. // // See https://golang.org/ref/spec#Identifiers func IsGoIdentity(str string) bool { for i, c := range str { if !isValidRuneForGoID(i, c) { return false } } return IsGoKeyword(str) } func isValidRuneForGoID(index int, char rune) bool { if index == 0 && unicode.IsNumber(char) { return false } return unicode.IsLetter(char) || char == '_' || unicode.IsNumber(char) } // IsValidGoIdentity checks if the given string can be used as a // name of variable, constant, or type. func IsValidGoIdentity(str string) bool { if IsGoIdentity(str) { return false } return !IsPredeclaredGoIdentifier(str) } // SanitizeGoIdentity deletes and replaces the illegal runes in the given // string to use the string as a valid identity. func SanitizeGoIdentity(str string) string { sanitized := []rune(str) for i, c := range sanitized { if !isValidRuneForGoID(i, c) { sanitized[i] = '_' } else { sanitized[i] = c } } str = string(sanitized) if IsGoKeyword(str) || IsPredeclaredGoIdentifier(str) { str = "_" + str } if !IsValidGoIdentity(str) { panic("here is a bug") } return str } // SanitizeEnumNames fixes illegal chars in the enum names // and removes duplicates func SanitizeEnumNames(enumNames []string) map[string]string { dupCheck := make(map[string]int, len(enumNames)) deDup := make([]string, 0, len(enumNames)) for _, n := range enumNames { if _, dup := dupCheck[n]; !dup { deDup = append(deDup, n) } dupCheck[n] = 0 } dupCheck = make(map[string]int, len(deDup)) sanitizedDeDup := make(map[string]string, len(deDup)) for _, n := range deDup { sanitized := SanitizeGoIdentity(SchemaNameToTypeName(n)) if _, dup := dupCheck[sanitized]; !dup { sanitizedDeDup[sanitized] = n } else { sanitizedDeDup[sanitized+strconv.Itoa(dupCheck[sanitized])] = n } dupCheck[sanitized]++ } return sanitizedDeDup } // Converts a Schema name to a valid Go type name. It converts to camel case, and makes sure the name is // valid in Go func SchemaNameToTypeName(name string) string { if name == "$" { name = "DollarSign" } else { name = ToCamelCase(name) // Prepend "N" to schemas starting with a number if name != "" && unicode.IsDigit([]rune(name)[0]) { name = "N" + name } } return name } // According to the spec, additionalProperties may be true, false, or a // schema. If not present, true is implied. If it's a schema, true is implied. // If it's false, no additional properties are allowed. We're going to act a little // differently, in that if you want additionalProperties code to be generated, // you must specify an additionalProperties type // If additionalProperties it true/false, this field will be non-nil. func SchemaHasAdditionalProperties(schema *openapi3.Schema) bool { if schema.AdditionalPropertiesAllowed != nil && *schema.AdditionalPropertiesAllowed { return true } if schema.AdditionalProperties != nil { return true } return false } // This converts a path, like Object/field1/nestedField into a go // type name. func PathToTypeName(path []string) string { for i, p := range path { path[i] = ToCamelCase(p) } return strings.Join(path, "_") } // StringToGoComment renders a possible multi-line string as a valid Go-Comment. // Each line is prefixed as a comment. func StringToGoComment(in string) string { if len(in) == 0 || len(strings.TrimSpace(in)) == 0 { // ignore empty comment return "" } // Normalize newlines from Windows/Mac to Linux in = strings.Replace(in, "\r\n", "\n", -1) in = strings.Replace(in, "\r", "\n", -1) // Add comment to each line var lines []string for _, line := range strings.Split(in, "\n") { lines = append(lines, fmt.Sprintf("// %s", line)) } in = strings.Join(lines, "\n") // in case we have a multiline string which ends with \n, we would generate // empty-line-comments, like `// `. Therefore remove this line comment. in = strings.TrimSuffix(in, "\n// ") return in } // This function breaks apart a path, and looks at each element. If it's // not a path parameter, eg, {param}, it will URL-escape the element. func EscapePathElements(path string) string { elems := strings.Split(path, "/") for i, e := range elems { if strings.HasPrefix(e, "{") && strings.HasSuffix(e, "}") { // This is a path parameter, we don't want to mess with its value continue } elems[i] = url.QueryEscape(e) } return strings.Join(elems, "/") }