599 lines
15 KiB
Go
599 lines
15 KiB
Go
// 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, "/")
|
|
}
|