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