Repository: deepmap/oapi-codegen (go)

Issue:
Add support for printing out the description as part of a field
The field defintions in openapi can have a description. It would be nice, if this description can be printed in the generated Go-code as well.

Pull request:
Fix #75 - Add support to print description for field
This feature allows printing description of a field
of a type prior when printing the field itself.

e.g.:

Old Version:

```go
// GetAlertemailSettingParams defines parameters for GetAlertemailSetting.
type GetAlertemailSettingParams struct {
	Format *[]string `json:"format,omitempty"`
	Filter *[]string `json:"filter,omitempty"`
}
```

New Version/This feature

```go
// GetAlertemailSettingParams defines parameters for GetAlertemailSetting.
type GetAlertemailSettingParams struct {

	// List of property names to include in results, separated by | (i.e. policyid|srcintf).
	Format *[]string `json:"format,omitempty"`

	// Filtering multiple key/value pairs
	// Operator     |   Description
	// ==     |   Case insensitive match with pattern.
	// !=     |    Does not match with pattern (case insensitive).
	// =@     |    Pattern found in object value (case insensitive).
	// !@     |    Pattern not found in object value (case insensitive).
	// <=     |    Value must be less than or equal to pattern.
	// <     |    Value must be less than pattern.
	// .>=    |    Value must be greater than or equal to pattern.
	// .>     |    Value must be greater than pattern.
	// Logical OR    |    Separate filters using commas ','
	// Logical AND    |    Filter strings can be combined to create logical AND queries by including multiple filters in the request.
	// Combining AND and OR    |    You can combine AND and OR filters together to create more complex filters.
	Filter *[]string `json:"filter,omitempty"`
}
```

Patch:
diff --git a/README.md b/README.md
index c99d50d..cdcd382 100644
--- a/README.md
+++ b/README.md
@@ -339,8 +339,9 @@ you can specify any combination of those.
 - `client`: generate the client boilerplate. It, too, requires the types to be
  present in its package.
 - `spec`: embed the OpenAPI spec into the generated code as a gzipped blob. This
- is useful for creating runtime validators.
- 
+- `skip-fmt`: skip running `go fmt` on the generated code. This is useful for debugging
+ the generated file in case the spec contains weird strings.
+
 So, for example, if you would like to produce only the server code, you could
 run `oapi-generate --generate types,server`. You could generate `types` and `server`
 into separate files, but both are required for the server code.  
diff --git a/cmd/oapi-codegen/oapi-codegen.go b/cmd/oapi-codegen/oapi-codegen.go
index 2fd4300..591c6f5 100644
--- a/cmd/oapi-codegen/oapi-codegen.go
+++ b/cmd/oapi-codegen/oapi-codegen.go
@@ -38,7 +38,7 @@ func main() {
 	)
 	flag.StringVar(&packageName, "package", "", "The package name for generated code")
 	flag.StringVar(&generate, "generate", "types,client,server,spec",
-		`Comma-separated list of code to generate; valid options: "types", client", "server", "spec"  (default types,client,server,"spec")`)
+		`Comma-separated list of code to generate; valid options: "types", client", "chi-server", "server", "skip-fmt", '"spec"  (default types,client,server,"spec")`)
 	flag.StringVar(&outputFile, "o", "", "Where to output generated code, stdout is default")
 	flag.Parse()
 
@@ -70,6 +70,8 @@ func main() {
 			opts.GenerateTypes = true
 		case "spec":
 			opts.EmbedSpec = true
+		case "skip-fmt":
+			opts.SkipFmt = true
 		default:
 			fmt.Printf("unknown generate option %s\n", g)
 			flag.PrintDefaults()
diff --git a/examples/petstore-expanded/chi/api/petstore.gen.go b/examples/petstore-expanded/chi/api/petstore.gen.go
index f7fe0e2..c30ebd8 100644
--- a/examples/petstore-expanded/chi/api/petstore.gen.go
+++ b/examples/petstore-expanded/chi/api/petstore.gen.go
@@ -38,8 +38,12 @@ type Pet struct {
 
 // FindPetsParams defines parameters for FindPets.
 type FindPetsParams struct {
-	Tags  *[]string `json:"tags,omitempty"`
-	Limit *int32    `json:"limit,omitempty"`
+
+	// tags to filter by
+	Tags *[]string `json:"tags,omitempty"`
+
+	// maximum number of results to return
+	Limit *int32 `json:"limit,omitempty"`
 }
 
 // addPetJSONBody defines parameters for AddPet.
diff --git a/examples/petstore-expanded/echo/api/petstore-types.gen.go b/examples/petstore-expanded/echo/api/petstore-types.gen.go
index 161a89d..a08e862 100644
--- a/examples/petstore-expanded/echo/api/petstore-types.gen.go
+++ b/examples/petstore-expanded/echo/api/petstore-types.gen.go
@@ -25,8 +25,12 @@ type Pet struct {
 
 // FindPetsParams defines parameters for FindPets.
 type FindPetsParams struct {
-	Tags  *[]string `json:"tags,omitempty"`
-	Limit *int32    `json:"limit,omitempty"`
+
+	// tags to filter by
+	Tags *[]string `json:"tags,omitempty"`
+
+	// maximum number of results to return
+	Limit *int32 `json:"limit,omitempty"`
 }
 
 // addPetJSONBody defines parameters for AddPet.
diff --git a/examples/petstore-expanded/petstore-client.gen.go b/examples/petstore-expanded/petstore-client.gen.go
index 46bfdbf..292ee71 100644
--- a/examples/petstore-expanded/petstore-client.gen.go
+++ b/examples/petstore-expanded/petstore-client.gen.go
@@ -37,8 +37,12 @@ type Pet struct {
 
 // FindPetsParams defines parameters for FindPets.
 type FindPetsParams struct {
-	Tags  *[]string `json:"tags,omitempty"`
-	Limit *int32    `json:"limit,omitempty"`
+
+	// tags to filter by
+	Tags *[]string `json:"tags,omitempty"`
+
+	// maximum number of results to return
+	Limit *int32 `json:"limit,omitempty"`
 }
 
 // addPetJSONBody defines parameters for AddPet.
diff --git a/pkg/codegen/codegen.go b/pkg/codegen/codegen.go
index f030dff..bc839a1 100644
--- a/pkg/codegen/codegen.go
+++ b/pkg/codegen/codegen.go
@@ -36,6 +36,7 @@ type Options struct {
 	GenerateClient     bool // GenerateClient specifies whether to generate client boilerplate
 	GenerateTypes      bool // GenerateTypes specifies whether to generate type definitions
 	EmbedSpec          bool // Whether to embed the swagger spec in the generated code
+	SkipFmt            bool // Whether to skip go fmt on the generated code
 }
 
 // Uses the Go templating engine to generate all of our server wrappers from
@@ -232,6 +233,9 @@ func Generate(swagger *openapi3.Swagger, packageName string, opts Options) (stri
 
 	// The generation code produces unindented horrors. Use the Go formatter
 	// to make it all pretty.
+	if opts.SkipFmt {
+		return goCode, nil
+	}
 	outBytes, err := format.Source([]byte(goCode))
 	if err != nil {
 		fmt.Println(goCode)
diff --git a/pkg/codegen/operations.go b/pkg/codegen/operations.go
index 28f39b4..adee5cd 100644
--- a/pkg/codegen/operations.go
+++ b/pkg/codegen/operations.go
@@ -577,6 +577,7 @@ func GenerateParamsTypes(op OperationDefinition) []TypeDefinition {
 			})
 		}
 		prop := Property{
+			Description:   param.Spec.Description,
 			JsonFieldName: param.ParamName,
 			Required:      param.Required,
 			Schema:        pSchema,
diff --git a/pkg/codegen/schema.go b/pkg/codegen/schema.go
index 04cedb3..316e098 100644
--- a/pkg/codegen/schema.go
+++ b/pkg/codegen/schema.go
@@ -53,6 +53,7 @@ func (s Schema) GetAdditionalTypeDefs() []TypeDefinition {
 }
 
 type Property struct {
+	Description   string
 	JsonFieldName string
 	Schema        Schema
 	Required      bool
@@ -82,11 +83,19 @@ func PropertiesEqual(a, b Property) bool {
 }
 
 func GenerateGoSchema(sref *openapi3.SchemaRef, path []string) (Schema, error) {
-	schema := sref.Value
-
 	// If Ref is set on the SchemaRef, it means that this type is actually a reference to
 	// another type. We're not de-referencing, so simply use the referenced type.
 	var refType string
+
+	// Add a fallback value in case the sref is nil.
+	// i.e. the parent schema defines a type:array, but the array has
+	// no items defined. Therefore we have at least valid Go-Code.
+	if sref == nil {
+		return Schema{GoType: "interface{}", RefType: refType}, nil
+	}
+
+	schema := sref.Value
+
 	if sref.Ref != "" {
 		var err error
 		// Convert the reference path to Go type
@@ -273,7 +282,14 @@ type FieldDescriptor struct {
 func GenFieldsFromProperties(props []Property) []string {
 	var fields []string
 	for _, p := range props {
-		field := fmt.Sprintf("    %s %s", p.GoFieldName(), p.GoTypeDef())
+		field := ""
+		// Add a comment to a field in case we have one, otherwise skip.
+		if p.Description != "" {
+			// Separate the comment from a previous-defined, unrelated field.
+			// Make sure the actual field is separated by a newline.
+			field += fmt.Sprintf("\n%s\n", StringToGoComment(p.Description))
+		}
+		field += fmt.Sprintf("    %s %s", p.GoFieldName(), p.GoTypeDef())
 		if p.Required {
 			field += fmt.Sprintf(" `json:\"%s\"`", p.JsonFieldName)
 		} else {
diff --git a/pkg/codegen/utils.go b/pkg/codegen/utils.go
index b10bc1b..b6e406d 100644
--- a/pkg/codegen/utils.go
+++ b/pkg/codegen/utils.go
@@ -53,10 +53,11 @@ func LowercaseFirstCharacter(str string) string {
 }
 
 // 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"
-// would be converted to WordWordWordWord
+// 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 := []string{".", "-", "_", "~", " "}
+	separators := []string{".", "-", "+", ":", ";", "_", "~", " ", "(", ")", "{", "}", "[", "]"}
 	in := []string{str}
 	out := make([]string, 0)
 
@@ -344,3 +345,23 @@ func PathToTypeName(path []string) string {
 	}
 	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 {
+	// 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
+}


Test patch:
diff --git a/internal/test/components/components.gen.go b/internal/test/components/components.gen.go
index 1dbbe93..43cece8 100644
--- a/internal/test/components/components.gen.go
+++ b/internal/test/components/components.gen.go
@@ -91,7 +91,12 @@ type ParamsWithAddPropsParams_P1 struct {
 
 // ParamsWithAddPropsParams defines parameters for ParamsWithAddProps.
 type ParamsWithAddPropsParams struct {
+
+	// This parameter has additional properties
 	P1 ParamsWithAddPropsParams_P1 `json:"p1"`
+
+	// This parameter has an anonymous inner property which needs to be
+	// turned into a proper type for additionalProperties to work
 	P2 struct {
 		Inner ParamsWithAddPropsParams_P2_Inner `json:"inner"`
 	} `json:"p2"`
diff --git a/internal/test/parameters/parameters.gen.go b/internal/test/parameters/parameters.gen.go
index f82dd4e..5ee3db1 100644
--- a/internal/test/parameters/parameters.gen.go
+++ b/internal/test/parameters/parameters.gen.go
@@ -33,34 +33,76 @@ type Object struct {
 
 // GetCookieParams defines parameters for GetCookie.
 type GetCookieParams struct {
-	P  *int32         `json:"p,omitempty"`
-	Ep *int32         `json:"ep,omitempty"`
-	Ea *[]int32       `json:"ea,omitempty"`
-	A  *[]int32       `json:"a,omitempty"`
-	Eo *Object        `json:"eo,omitempty"`
-	O  *Object        `json:"o,omitempty"`
+
+	// primitive
+	P *int32 `json:"p,omitempty"`
+
+	// primitive
+	Ep *int32 `json:"ep,omitempty"`
+
+	// exploded array
+	Ea *[]int32 `json:"ea,omitempty"`
+
+	// array
+	A *[]int32 `json:"a,omitempty"`
+
+	// exploded object
+	Eo *Object `json:"eo,omitempty"`
+
+	// object
+	O *Object `json:"o,omitempty"`
+
+	// complex object
 	Co *ComplexObject `json:"co,omitempty"`
 }
 
 // GetHeaderParams defines parameters for GetHeader.
 type GetHeaderParams struct {
-	XPrimitive         *int32         `json:"X-Primitive,omitempty"`
-	XPrimitiveExploded *int32         `json:"X-Primitive-Exploded,omitempty"`
-	XArrayExploded     *[]int32       `json:"X-Array-Exploded,omitempty"`
-	XArray             *[]int32       `json:"X-Array,omitempty"`
-	XObjectExploded    *Object        `json:"X-Object-Exploded,omitempty"`
-	XObject            *Object        `json:"X-Object,omitempty"`
-	XComplexObject     *ComplexObject `json:"X-Complex-Object,omitempty"`
+
+	// primitive
+	XPrimitive *int32 `json:"X-Primitive,omitempty"`
+
+	// primitive
+	XPrimitiveExploded *int32 `json:"X-Primitive-Exploded,omitempty"`
+
+	// exploded array
+	XArrayExploded *[]int32 `json:"X-Array-Exploded,omitempty"`
+
+	// array
+	XArray *[]int32 `json:"X-Array,omitempty"`
+
+	// exploded object
+	XObjectExploded *Object `json:"X-Object-Exploded,omitempty"`
+
+	// object
+	XObject *Object `json:"X-Object,omitempty"`
+
+	// complex object
+	XComplexObject *ComplexObject `json:"X-Complex-Object,omitempty"`
 }
 
 // GetQueryFormParams defines parameters for GetQueryForm.
 type GetQueryFormParams struct {
-	Ea *[]int32       `json:"ea,omitempty"`
-	A  *[]int32       `json:"a,omitempty"`
-	Eo *Object        `json:"eo,omitempty"`
-	O  *Object        `json:"o,omitempty"`
-	Ep *int32         `json:"ep,omitempty"`
-	P  *int32         `json:"p,omitempty"`
+
+	// exploded array
+	Ea *[]int32 `json:"ea,omitempty"`
+
+	// array
+	A *[]int32 `json:"a,omitempty"`
+
+	// exploded object
+	Eo *Object `json:"eo,omitempty"`
+
+	// object
+	O *Object `json:"o,omitempty"`
+
+	// exploded primitive
+	Ep *int32 `json:"ep,omitempty"`
+
+	// primitive
+	P *int32 `json:"p,omitempty"`
+
+	// complex object
 	Co *ComplexObject `json:"co,omitempty"`
 }
 
diff --git a/pkg/codegen/codegen_test.go b/pkg/codegen/codegen_test.go
index d39384f..9ca8db7 100644
--- a/pkg/codegen/codegen_test.go
+++ b/pkg/codegen/codegen_test.go
@@ -102,6 +102,19 @@ func TestExampleOpenAPICodeGeneration(t *testing.T) {
 	// Check that response structs are generated correctly:
 	assert.Contains(t, code, "type getTestByNameResponse struct {")
 
+	// Check that response structs contains fallbacks to interface for invalid types:
+	// Here an invalid array with no items.
+	assert.Contains(t, code, `
+type getTestByNameResponse struct {
+	Body         []byte
+	HTTPResponse *http.Response
+	JSON200      *[]Test
+	XML200       *[]Test
+	JSON422      *[]interface{}
+	XML422       *[]interface{}
+	JSONDefault  *Error
+}`)
+
 	// Check that the helper methods are generated correctly:
 	assert.Contains(t, code, "func (r getTestByNameResponse) Status() string {")
 	assert.Contains(t, code, "func (r getTestByNameResponse) StatusCode() int {")
@@ -157,6 +170,15 @@ paths:
                 type: array
                 items:
                   $ref: '#/components/schemas/Test'
+        422:
+          description: InvalidArray
+          content:
+            application/xml:
+              schema:
+                type: array
+            application/json:
+              schema:
+                type: array
         default:
           description: Error
           content:
diff --git a/pkg/codegen/utils_test.go b/pkg/codegen/utils_test.go
index 39f6d8d..5892832 100644
--- a/pkg/codegen/utils_test.go
+++ b/pkg/codegen/utils_test.go
@@ -22,7 +22,7 @@ import (
 
 func TestStringOps(t *testing.T) {
 	// Test that each substitution works
-	assert.Equal(t, "WordWordWORDWordWord", ToCamelCase("word.word-WORD_word~word"), "Camel case conversion failed")
+	assert.Equal(t, "WordWordWORDWordWordWordWordWordWordWordWordWordWord", ToCamelCase("word.word-WORD+Word_word~word(Word)Word{Word}Word[Word]Word:Word;"), "Camel case conversion failed")
 
 	// Make sure numbers don't interact in a funny way.
 	assert.Equal(t, "Number1234", ToCamelCase("number-1234"), "Number Camelcasing not working.")
@@ -181,3 +181,55 @@ func TestReplacePathParamsWithStr(t *testing.T) {
 	result := ReplacePathParamsWithStr("/path/{param1}/{.param2}/{;param3*}/foo")
 	assert.EqualValues(t, "/path/%s/%s/%s/foo", result)
 }
+
+func TestStringToGoComment(t *testing.T) {
+	testCases := []struct {
+		input    string
+		expected string
+		message  string
+	}{
+		{
+			input:    "",
+			expected: "// ",
+			message:  "blank string should be preserved with comment",
+		},
+		{
+			input:    " ",
+			expected: "//  ",
+			message:  "whitespace should be preserved with comment",
+		},
+		{
+			input:    "Single Line",
+			expected: "// Single Line",
+			message:  "single line comment",
+		},
+		{
+			input:    "    Single Line",
+			expected: "//     Single Line",
+			message:  "single line comment preserving whitespace",
+		},
+		{
+			input: `Multi
+Line
+  With
+    Spaces
+	And
+		Tabs
+`,
+			expected: `// Multi
+// Line
+//   With
+//     Spaces
+// 	And
+// 		Tabs`,
+			message: "multi line preserving whitespaces using tabs or spaces",
+		},
+	}
+	for _, testCase := range testCases {
+		t.Run(testCase.message, func(t *testing.T) {
+			result := StringToGoComment(testCase.input)
+			assert.EqualValues(t, testCase.expected, result, testCase.message)
+		})
+	}
+
+}