Repository: deepmap/oapi-codegen (go)

Issue:
Fallback to `interface{}` in case a type cannot be determined
In some weird OpenAPI specs, it is possible that you might have an `array` defined, but no `items`.
In the current implementation of oapi-codegen, this leads to a nullpointer-exception (NPE).

It would be better, if we could just fallback to an `interface` type. This way the generated Go-Code is fine and will compile, and it will be up to the user of the generated Go-Library to cast/map the `interface{}` to the appropiate Go-struct. It should not be a concern that the openapi-spec is crap, as long as we have `interface{}` available as a placeholder.

Pull request description:
Fix #73: interface{} type as fallback for invalid type
This adds a fallback behaviour in case a type cannot
be determined, where one should be expected.

This is for example the case if one defines a type `array`
but does not add any `items` definitions to it.

Fixes:

- #73

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/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/schema.go b/pkg/codegen/schema.go
index 04cedb3..12a1b78 100644
--- a/pkg/codegen/schema.go
+++ b/pkg/codegen/schema.go
@@ -82,11 +82,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
diff --git a/pkg/codegen/utils.go b/pkg/codegen/utils.go
index b10bc1b..5b8f883 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)
 


Test patch:
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..db4582b 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.")