Repository: deepmap/oapi-codegen
Pull request #191 (2020-06-07 14:50:01)

Add email format type
Add format value called `email` for string type. Use regex expression matched in very popular validation library [go-playground/validator](https://github.com/go-playground/validator).

- Added marshal / unmarshal unit test to `email` type
- Added `email` type to internal test schema


Patch:
diff --git a/README.md b/README.md
index 6f7d9ae..1d5d39a 100644
--- a/README.md
+++ b/README.md
@@ -440,6 +440,8 @@ you can specify any combination of those.
  the generated file in case the spec contains weird strings.
 - `skip-prune`: skip pruning unused components from the spec prior to generating
  the code.
+- `import-mapping`: specifies a map of references external OpenAPI specs to go
+ Go include paths. Please see below.
 
 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
@@ -453,6 +455,28 @@ To generate a server that only handles `admin` paths, use the argument
 `-include-tags="admin"`. When neither of these arguments is present, all paths
 are generated.
 
+### Import Mappings
+
+OpenAPI specifications may contain references to other OpenAPI specifications,
+and we need some additional information in order to be able to generate correct
+Go code.
+
+An external reference looks like this:
+
+    $ref: ./some_spec.yaml#/components/schemas/Type
+    
+We assume that you have already generated the boilerplate code for `./some_spec.yaml`
+using `oapi-codegen`, and you have a package which contains the generated code,
+let's call it `github.com/deepmap/some-package`. You need to tell `oapi-codegen` that
+`some_spec.yaml` corresponds to this package, and you would do it by specifying
+this command line argument:
+
+    -import-mapping=./some_spec.yaml:github.com/deepmap/some-package
+    
+This tells us that in order to resolve references generated from `some_spec.yaml` we
+need to import `github.com/deepmap/some-package`. You may specify multiple mappings
+by comma separating them in the form `key1:value1,key2:value2`.
+    
 ## What's missing or incomplete
 
 This code is still young, and not complete, since we're filling it in as we
diff --git a/cmd/oapi-codegen/oapi-codegen.go b/cmd/oapi-codegen/oapi-codegen.go
index b578e9f..6a6b523 100644
--- a/cmd/oapi-codegen/oapi-codegen.go
+++ b/cmd/oapi-codegen/oapi-codegen.go
@@ -33,12 +33,13 @@ func errExit(format string, args ...interface{}) {
 
 func main() {
 	var (
-		packageName  string
-		generate     string
-		outputFile   string
-		includeTags  string
-		excludeTags  string
-		templatesDir string
+		packageName   string
+		generate      string
+		outputFile    string
+		includeTags   string
+		excludeTags   string
+		templatesDir  string
+		importMapping string
 	)
 	flag.StringVar(&packageName, "package", "", "The package name for generated code")
 	flag.StringVar(&generate, "generate", "types,client,server,spec",
@@ -47,6 +48,7 @@ func main() {
 	flag.StringVar(&includeTags, "include-tags", "", "Only include operations with the given tags. Comma-separated list of tags.")
 	flag.StringVar(&excludeTags, "exclude-tags", "", "Exclude operations that are tagged with the given tags. Comma-separated list of tags.")
 	flag.StringVar(&templatesDir, "templates", "", "Path to directory containing user templates")
+	flag.StringVar(&importMapping, "import-mapping", "", "A dict from the external reference to golang package path")
 	flag.Parse()
 
 	if flag.NArg() < 1 {
@@ -106,6 +108,13 @@ func main() {
 	}
 	opts.UserTemplates = templates
 
+	if len(importMapping) > 0 {
+		opts.ImportMapping, err = util.ParseCommandlineMap(importMapping)
+		if err != nil {
+			errExit("error parsing import-mapping: %s\n", err)
+		}
+	}
+
 	code, err := codegen.Generate(swagger, packageName, opts)
 	if err != nil {
 		errExit("error generating code: %s\n", err)
@@ -159,3 +168,4 @@ func loadTemplateOverrides(templatesDir string) (map[string]string, error) {
 
 	return templates, nil
 }
+
diff --git a/go.mod b/go.mod
index 758e100..1016256 100644
--- a/go.mod
+++ b/go.mod
@@ -2,22 +2,25 @@ module github.com/deepmap/oapi-codegen
 
 require (
 	github.com/cyberdelia/templates v0.0.0-20141128023046-ca7fffd4298c
-	github.com/getkin/kin-openapi v0.3.0
+	github.com/davecgh/go-spew v1.1.1 // indirect
+	github.com/getkin/kin-openapi v0.13.0
 	github.com/go-chi/chi v4.0.2+incompatible
 	github.com/golangci/lint-1 v0.0.0-20181222135242-d2cdd8c08219
+	github.com/kr/pretty v0.1.0 // indirect
 	github.com/labstack/echo/v4 v4.1.11
 	github.com/matryer/moq v0.0.0-20190312154309-6cfb0558e1bd
 	github.com/mattn/go-colorable v0.1.4 // indirect
 	github.com/mattn/go-isatty v0.0.10 // indirect
 	github.com/pkg/errors v0.8.1
-	github.com/stretchr/testify v1.4.0
+	github.com/stretchr/testify v1.5.1
 	github.com/valyala/fasttemplate v1.1.0 // indirect
 	golang.org/x/crypto v0.0.0-20191112222119-e1110fd1c708 // indirect
 	golang.org/x/net v0.0.0-20191112182307-2180aed22343 // indirect
 	golang.org/x/sys v0.0.0-20191115151921-52ab43148777 // indirect
 	golang.org/x/text v0.3.2 // indirect
 	golang.org/x/tools v0.0.0-20191125144606-a911d9008d1f // indirect
-	gopkg.in/yaml.v2 v2.2.8
+	gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 // indirect
+	gopkg.in/yaml.v2 v2.3.0
 )
 
 go 1.13
diff --git a/go.sum b/go.sum
index cea98d0..1fb5ee4 100644
--- a/go.sum
+++ b/go.sum
@@ -6,8 +6,8 @@ github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c
 github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
 github.com/dgrijalva/jwt-go v3.2.0+incompatible h1:7qlOGliEKZXTDg6OTjfoBKDXWrumCAMpl/TFQ4/5kLM=
 github.com/dgrijalva/jwt-go v3.2.0+incompatible/go.mod h1:E3ru+11k8xSBh+hMPgOLZmtrrCbhqsmaPHjLKYnJCaQ=
-github.com/getkin/kin-openapi v0.3.0 h1:xsQ4mA20YJDMgIHdHqMKZ66QUe6/hi+x6yLsTTz8xyQ=
-github.com/getkin/kin-openapi v0.3.0/go.mod h1:W8dhxZgpE84ciM+VIItFqkmZ4eHtuomrdIHtASQIqi0=
+github.com/getkin/kin-openapi v0.13.0 h1:03fqBEEgivp4MVK2ElB140B56hjO9ZFvFTHBsvFsSro=
+github.com/getkin/kin-openapi v0.13.0/go.mod h1:WGRs2ZMM1Q8LR1QBEwUxC6RJEfaBcD0s+pcEVXFuAjw=
 github.com/ghodss/yaml v1.0.0 h1:wQHKEahhL6wmXdzwWG11gIVCkOv05bNOh+Rxn0yngAk=
 github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04=
 github.com/go-chi/chi v4.0.2+incompatible h1:maB6vn6FqCxrpz4FqWdh4+lwpyZIQS7YEAUcHlgXVRs=
@@ -41,10 +41,10 @@ github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZb
 github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
 github.com/stretchr/objx v0.1.0 h1:4G4v2dO3VZwixGIRoQ5Lfboy6nUhCyYzaqnIAPPhYs4=
 github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
-github.com/stretchr/testify v1.3.0 h1:TivCn/peBQ7UY8ooIcPgZFpTNSz0Q2U6UrFlUfqbe0Q=
-github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
 github.com/stretchr/testify v1.4.0 h1:2E4SXV/wtOkTonXsotYi4li6zVWxYlZuYNCXe9XRJyk=
 github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
+github.com/stretchr/testify v1.5.1 h1:nOGnQDM7FYENwehXlg/kFVnos3rEvtKTjRvOWSzb6H4=
+github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA=
 github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw=
 github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc=
 github.com/valyala/fasttemplate v1.0.1 h1:tY9CJiPnMXf1ERmG2EyK7gNUd+c6RKGD0IfU8WdUSz8=
@@ -85,5 +85,5 @@ gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 h1:qIbj1fsPNlZgppZ+VLlY7N33
 gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
 gopkg.in/yaml.v2 v2.2.2 h1:ZCJp+EgiOT7lHqUV2J862kp8Qj64Jo6az82+3Td9dZw=
 gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
-gopkg.in/yaml.v2 v2.2.8 h1:obN1ZagJSUGI0Ek/LBmuj4SNLPfIny3KsKFopxRdj10=
-gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
+gopkg.in/yaml.v2 v2.3.0 h1:clyUAQHOM3G0M3f5vQj7LuJrETvjVot3Z5el9nffUtU=
+gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
diff --git a/pkg/codegen/codegen.go b/pkg/codegen/codegen.go
index 9f12de5..a4b979e 100644
--- a/pkg/codegen/codegen.go
+++ b/pkg/codegen/codegen.go
@@ -42,6 +42,7 @@ type Options struct {
 	IncludeTags        []string          // Only include operations that have one of these tags. Ignored when empty.
 	ExcludeTags        []string          // Exclude operations that have one of these tags. Ignored when empty.
 	UserTemplates      map[string]string // Override built-in templates from user-provided files
+	ImportMapping      map[string]string // ImportMapping specifies the golang package path for each external reference
 }
 
 type goImport struct {
@@ -84,12 +85,41 @@ var (
 		{lookFor: "xml\\.", packageName: "encoding/xml"},
 		{lookFor: "yaml\\.", packageName: "gopkg.in/yaml.v2"},
 	}
+
+	importMapping = map[string]goImport{}
 )
 
+func constructImportMapping(input map[string]string) map[string]goImport {
+	var (
+		nameToAlias = map[string]string{}
+		result      = map[string]goImport{}
+	)
+
+	{
+		var packagePaths []string
+		for _, packageName := range input {
+			packagePaths = append(packagePaths, packageName)
+		}
+		sort.Strings(packagePaths)
+
+		for _, packageName := range packagePaths {
+			if _, ok := nameToAlias[packageName]; !ok {
+				nameToAlias[packageName] = fmt.Sprintf("externalRef%d", len(nameToAlias))
+			}
+		}
+	}
+	for urlOrPath, packageName := range input {
+		result[urlOrPath] = goImport{alias: nameToAlias[packageName], packageName: packageName}
+	}
+	return result
+}
+
 // Uses the Go templating engine to generate all of our server wrappers from
 // the descriptions we've built up above from the schema objects.
 // opts defines
 func Generate(swagger *openapi3.Swagger, packageName string, opts Options) (string, error) {
+	importMapping = constructImportMapping(opts.ImportMapping)
+
 	filterOperationsByTag(swagger, opts)
 	if !opts.SkipPrune {
 		pruneUnusedComponents(swagger)
@@ -169,6 +199,9 @@ func Generate(swagger *openapi3.Swagger, packageName string, opts Options) (stri
 
 	// Imports needed for the generated code to compile
 	var imports []string
+	for _, importGo := range importMapping {
+		imports = append(imports, importGo.String())
+	}
 
 	var buf bytes.Buffer
 	w := bufio.NewWriter(&buf)
diff --git a/pkg/codegen/schema.go b/pkg/codegen/schema.go
index 3124017..8cdf9dc 100644
--- a/pkg/codegen/schema.go
+++ b/pkg/codegen/schema.go
@@ -13,6 +13,8 @@ type Schema struct {
 	GoType  string // The Go type needed to represent the schema
 	RefType string // If the type has a type name, this is set
 
+	EnumValues []string // Enum values
+
 	Properties               []Property       // For an object, the fields with names
 	HasAdditionalProperties  bool             // Whether we support additional properties
 	AdditionalPropertiesType *Schema          // And if we do, their type
@@ -251,10 +253,15 @@ func GenerateGoSchema(sref *openapi3.SchemaRef, path []string) (Schema, error) {
 			}
 			outSchema.GoType = "bool"
 		case "string":
+			for _, enumValue := range schema.Enum {
+				outSchema.EnumValues = append(outSchema.EnumValues, enumValue.(string))
+			}
 			// Special case string formats here.
 			switch f {
 			case "byte":
 				outSchema.GoType = "[]byte"
+			case "email":
+				outSchema.GoType = "openapi_types.Email"
 			case "date":
 				outSchema.GoType = "openapi_types.Date"
 			case "date-time":
diff --git a/pkg/codegen/templates/templates.gen.go b/pkg/codegen/templates/templates.gen.go
index 04e7fc3..491eda3 100644
--- a/pkg/codegen/templates/templates.gen.go
+++ b/pkg/codegen/templates/templates.gen.go
@@ -754,6 +754,15 @@ type ServerInterface interface {
 	"typedef.tmpl": `{{range .Types}}
 // {{.TypeName}} defines model for {{.JsonName}}.
 type {{.TypeName}} {{.Schema.TypeDecl}}
+{{- if gt (len .Schema.EnumValues) 0 }}
+// List of {{ .TypeName }}
+const (
+	{{- $typeName := .TypeName }}
+    {{- range .Schema.EnumValues }}
+    {{ $typeName }}_{{ . }} {{ $typeName }} = "{{ . }}"
+    {{- end }}
+)
+{{- end }}
 {{end}}
 `,
 	"wrappers.tmpl": `// ServerInterfaceWrapper converts echo contexts to parameters.
diff --git a/pkg/codegen/templates/typedef.tmpl b/pkg/codegen/templates/typedef.tmpl
index d93847a..4f20b07 100644
--- a/pkg/codegen/templates/typedef.tmpl
+++ b/pkg/codegen/templates/typedef.tmpl
@@ -1,4 +1,13 @@
 {{range .Types}}
 // {{.TypeName}} defines model for {{.JsonName}}.
 type {{.TypeName}} {{.Schema.TypeDecl}}
+{{- if gt (len .Schema.EnumValues) 0 }}
+// List of {{ .TypeName }}
+const (
+	{{- $typeName := .TypeName }}
+    {{- range .Schema.EnumValues }}
+    {{ $typeName }}_{{ . }} {{ $typeName }} = "{{ . }}"
+    {{- end }}
+)
+{{- end }}
 {{end}}
diff --git a/pkg/codegen/utils.go b/pkg/codegen/utils.go
index 2a35b03..bcef998 100644
--- a/pkg/codegen/utils.go
+++ b/pkg/codegen/utils.go
@@ -65,7 +65,7 @@ func ToCamelCase(str string) string {
 		if unicode.IsUpper(v) {
 			n += string(v)
 		}
-			if unicode.IsDigit(v) {
+		if unicode.IsDigit(v) {
 			n += string(v)
 		}
 		if unicode.IsLower(v) {
@@ -76,7 +76,7 @@ func ToCamelCase(str string) string {
 			}
 		}
 
-		 if strings.ContainsRune(separators, v) {
+		if strings.ContainsRune(separators, v) {
 			capNext = true
 		} else {
 			capNext = false
@@ -197,20 +197,31 @@ func StringInArray(str string, array []string) bool {
 // #/components/schemas/Foo -> Foo
 // #/components/parameters/Bar -> Bar
 // #/components/responses/Baz -> Baz
-// Remote components (document.json#/Foo) are not yet supported
-// URL components (http://deepmap.com/schemas/document.json#Foo) are not yet
-// supported
-// We only support flat components for now, so no components in a schema under
-// components.
+// 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
+//
 func RefPathToGoType(refPath string) (string, error) {
-	pathParts := strings.Split(refPath, "/")
-	if pathParts[0] != "#" {
-		return "", errors.New("Only local document components are supported")
+	if refPath[0] == '#' {
+		pathParts := strings.Split(refPath, "/")
+		if len(pathParts) != 4 {
+			return "", errors.New("Parameter nesting is deeper than supported")
+		}
+		return SchemaNameToTypeName(pathParts[3]), nil
 	}
-	if len(pathParts) != 4 {
-		return "", errors.New("Parameter nesting is deeper than supported")
+	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)
+		if err != nil {
+			return "", err
+		}
+		return fmt.Sprintf("%s.%s", goImport.alias, goType), nil
 	}
-	return SchemaNameToTypeName(pathParts[3]), nil
 }
 
 // This function converts a swagger style path URI with parameters to a
diff --git a/pkg/types/email.go b/pkg/types/email.go
new file mode 100644
index 0000000..00a4cf6
--- /dev/null
+++ b/pkg/types/email.go
@@ -0,0 +1,27 @@
+package types
+
+import (
+	"encoding/json"
+	"errors"
+)
+
+type Email string
+
+func (e Email) MarshalJSON() ([]byte, error) {
+	if !emailRegex.MatchString(string(e)) {
+		return nil, errors.New("email: failed to pass regex validation")
+	}
+	return json.Marshal(string(e))
+}
+
+func (e *Email) UnmarshalJSON(data []byte) error {
+	var s string
+	if err := json.Unmarshal(data, &s); err != nil {
+		return err
+	}
+	if !emailRegex.MatchString(s) {
+		return errors.New("email: failed to pass regex validation")
+	}
+	*e = Email(s)
+	return nil
+}
diff --git a/pkg/types/regexes.go b/pkg/types/regexes.go
new file mode 100644
index 0000000..94f17df
--- /dev/null
+++ b/pkg/types/regexes.go
@@ -0,0 +1,11 @@
+package types
+
+import "regexp"
+
+const (
+	emailRegexString = "^(?:(?:(?:(?:[a-zA-Z]|\\d|[!#\\$%&'\\*\\+\\-\\/=\\?\\^_`{\\|}~]|[\\x{00A0}-\\x{D7FF}\\x{F900}-\\x{FDCF}\\x{FDF0}-\\x{FFEF}])+(?:\\.([a-zA-Z]|\\d|[!#\\$%&'\\*\\+\\-\\/=\\?\\^_`{\\|}~]|[\\x{00A0}-\\x{D7FF}\\x{F900}-\\x{FDCF}\\x{FDF0}-\\x{FFEF}])+)*)|(?:(?:\\x22)(?:(?:(?:(?:\\x20|\\x09)*(?:\\x0d\\x0a))?(?:\\x20|\\x09)+)?(?:(?:[\\x01-\\x08\\x0b\\x0c\\x0e-\\x1f\\x7f]|\\x21|[\\x23-\\x5b]|[\\x5d-\\x7e]|[\\x{00A0}-\\x{D7FF}\\x{F900}-\\x{FDCF}\\x{FDF0}-\\x{FFEF}])|(?:(?:[\\x01-\\x09\\x0b\\x0c\\x0d-\\x7f]|[\\x{00A0}-\\x{D7FF}\\x{F900}-\\x{FDCF}\\x{FDF0}-\\x{FFEF}]))))*(?:(?:(?:\\x20|\\x09)*(?:\\x0d\\x0a))?(\\x20|\\x09)+)?(?:\\x22))))@(?:(?:(?:[a-zA-Z]|\\d|[\\x{00A0}-\\x{D7FF}\\x{F900}-\\x{FDCF}\\x{FDF0}-\\x{FFEF}])|(?:(?:[a-zA-Z]|\\d|[\\x{00A0}-\\x{D7FF}\\x{F900}-\\x{FDCF}\\x{FDF0}-\\x{FFEF}])(?:[a-zA-Z]|\\d|-|\\.|~|[\\x{00A0}-\\x{D7FF}\\x{F900}-\\x{FDCF}\\x{FDF0}-\\x{FFEF}])*(?:[a-zA-Z]|\\d|[\\x{00A0}-\\x{D7FF}\\x{F900}-\\x{FDCF}\\x{FDF0}-\\x{FFEF}])))\\.)+(?:(?:[a-zA-Z]|[\\x{00A0}-\\x{D7FF}\\x{F900}-\\x{FDCF}\\x{FDF0}-\\x{FFEF}])|(?:(?:[a-zA-Z]|[\\x{00A0}-\\x{D7FF}\\x{F900}-\\x{FDCF}\\x{FDF0}-\\x{FFEF}])(?:[a-zA-Z]|\\d|-|\\.|~|[\\x{00A0}-\\x{D7FF}\\x{F900}-\\x{FDCF}\\x{FDF0}-\\x{FFEF}])*(?:[a-zA-Z]|[\\x{00A0}-\\x{D7FF}\\x{F900}-\\x{FDCF}\\x{FDF0}-\\x{FFEF}])))\\.?$"
+)
+
+var (
+	emailRegex = regexp.MustCompile(emailRegexString)
+)
diff --git a/pkg/util/inputmapping.go b/pkg/util/inputmapping.go
new file mode 100644
index 0000000..3557ce5
--- /dev/null
+++ b/pkg/util/inputmapping.go
@@ -0,0 +1,60 @@
+package util
+
+import (
+	"fmt"
+	"strings"
+)
+
+// The input mapping is experessed on the command line as `key1:value1,key2:value2,...`
+// We parse it here, but need to keep in mind that keys or values may contain
+// commas and colons. We will allow escaping those using double quotes, so
+// when passing in "key1":"value1", we will not look inside the quoted sections.
+func ParseCommandlineMap(src string) (map[string]string, error) {
+	result := make(map[string]string)
+	tuples := splitString(src, ',')
+	for _, t := range tuples {
+		kv := splitString(t, ':')
+		if len(kv) != 2 {
+			return nil, fmt.Errorf("expected key:value, got :%s", t)
+		}
+		key := strings.TrimLeft(kv[0], `"`)
+		key = strings.TrimRight(key, `"`)
+
+		value := strings.TrimLeft(kv[1], `"`)
+		value = strings.TrimRight(value, `"`)
+
+		result[key] = value
+	}
+	return result, nil
+}
+
+// This function splits a string along the specifed separator, but it
+// ignores anything between double quotes for splitting. We do simple
+// inside/outside quote counting. Quotes are not stripped from output.
+func splitString(s string, sep rune) ([]string) {
+	const escapeChar rune = '"'
+
+	var parts []string
+	var part string
+	inQuotes := false
+
+	for _, c := range s {
+		if c == escapeChar{
+			if inQuotes {
+				inQuotes = false
+			} else {
+				inQuotes = true
+			}
+		}
+
+		// If we've gotten the separator rune, consider the previous part
+		// complete, but only if we're outside of quoted sections
+		if c == sep && !inQuotes {
+			parts = append(parts, part)
+			part = ""
+			continue
+		}
+		part = part + string(c)
+	}
+	return append(parts, part)
+}
diff --git a/pkg/util/loader.go b/pkg/util/loader.go
index 6538e24..56b0297 100644
--- a/pkg/util/loader.go
+++ b/pkg/util/loader.go
@@ -1,32 +1,13 @@
 package util
 
 import (
-	"fmt"
-	"io/ioutil"
-	"path/filepath"
-	"strings"
-
 	"github.com/getkin/kin-openapi/openapi3"
 )
 
-func LoadSwagger(filePath string) (*openapi3.Swagger, error) {
-	data, err := ioutil.ReadFile(filePath)
-	if err != nil {
-		return nil, err
-	}
-
-	var swagger *openapi3.Swagger
-	ext := filepath.Ext(filePath)
-	ext = strings.ToLower(ext)
-	switch ext {
-	// The YAML handler can parse both YAML and JSON
-	case ".yaml", ".yml", ".json":
-		swagger, err = openapi3.NewSwaggerLoader().LoadSwaggerFromData(data)
-	default:
-		return nil, fmt.Errorf("%s is not a supported extension, use .yaml, .yml or .json", ext)
-	}
-	if err != nil {
-		return nil, err
-	}
-	return swagger, nil
+func LoadSwagger(filePath string) (swagger *openapi3.Swagger, err error) {
+	loader := openapi3.NewSwaggerLoader()
+	loader.IsExternalRefsAllowed = true
+	swagger, err = loader.LoadSwaggerFromFile(filePath)
+	return
 }
+


Test patch:
diff --git a/internal/test/externalref/doc.go b/internal/test/externalref/doc.go
new file mode 100644
index 0000000..e239ead
--- /dev/null
+++ b/internal/test/externalref/doc.go
@@ -0,0 +1,3 @@
+package externalref
+
+//go:generate go run github.com/deepmap/oapi-codegen/cmd/oapi-codegen -generate types,skip-prune --package=externalref -o externalref.gen.go --import-mapping=./packageA/spec.yaml:github.com/deepmap/oapi-codegen/internal/test/externalref/packageA,./packageB/spec.yaml:github.com/deepmap/oapi-codegen/internal/test/externalref/packageB spec.yaml
diff --git a/internal/test/externalref/externalref.gen.go b/internal/test/externalref/externalref.gen.go
new file mode 100644
index 0000000..5fc09bd
--- /dev/null
+++ b/internal/test/externalref/externalref.gen.go
@@ -0,0 +1,15 @@
+// Package externalref provides primitives to interact the openapi HTTP API.
+//
+// Code generated by github.com/deepmap/oapi-codegen DO NOT EDIT.
+package externalref
+
+import (
+	externalRef0 "github.com/deepmap/oapi-codegen/internal/test/externalref/packageA"
+	externalRef1 "github.com/deepmap/oapi-codegen/internal/test/externalref/packageB"
+)
+
+// Container defines model for Container.
+type Container struct {
+	ObjectA *externalRef0.ObjectA `json:"object_a,omitempty"`
+	ObjectB *externalRef1.ObjectB `json:"object_b,omitempty"`
+}
diff --git a/internal/test/externalref/imports_test.go b/internal/test/externalref/imports_test.go
new file mode 100644
index 0000000..284964a
--- /dev/null
+++ b/internal/test/externalref/imports_test.go
@@ -0,0 +1,16 @@
+package externalref
+
+import (
+	"testing"
+
+	"github.com/deepmap/oapi-codegen/internal/test/externalref/packageA"
+	"github.com/deepmap/oapi-codegen/internal/test/externalref/packageB"
+)
+
+func TestParameters(t *testing.T) {
+	b := &packageB.ObjectB{}
+	_ = Container{
+		ObjectA: &packageA.ObjectA{ObjectB: b},
+		ObjectB: b,
+	}
+}
diff --git a/internal/test/externalref/packageA/doc.go b/internal/test/externalref/packageA/doc.go
new file mode 100644
index 0000000..410765f
--- /dev/null
+++ b/internal/test/externalref/packageA/doc.go
@@ -0,0 +1,3 @@
+package packageA
+
+//go:generate go run github.com/deepmap/oapi-codegen/cmd/oapi-codegen -generate types,skip-prune --package=packageA -o externalref.gen.go --import-mapping=../packageB/spec.yaml:github.com/deepmap/oapi-codegen/internal/test/externalref/packageB spec.yaml
diff --git a/internal/test/externalref/packageA/externalref.gen.go b/internal/test/externalref/packageA/externalref.gen.go
new file mode 100644
index 0000000..066f2b7
--- /dev/null
+++ b/internal/test/externalref/packageA/externalref.gen.go
@@ -0,0 +1,14 @@
+// Package packageA provides primitives to interact the openapi HTTP API.
+//
+// Code generated by github.com/deepmap/oapi-codegen DO NOT EDIT.
+package packageA
+
+import (
+	externalRef0 "github.com/deepmap/oapi-codegen/internal/test/externalref/packageB"
+)
+
+// ObjectA defines model for ObjectA.
+type ObjectA struct {
+	Name    *string               `json:"name,omitempty"`
+	ObjectB *externalRef0.ObjectB `json:"object_b,omitempty"`
+}
diff --git a/internal/test/externalref/packageA/spec.yaml b/internal/test/externalref/packageA/spec.yaml
new file mode 100644
index 0000000..b2386a0
--- /dev/null
+++ b/internal/test/externalref/packageA/spec.yaml
@@ -0,0 +1,8 @@
+components:
+  schemas:
+    ObjectA:
+      properties:
+        name:
+          type: string
+        object_b:
+          $ref: ../packageB/spec.yaml#/components/schemas/ObjectB
\ No newline at end of file
diff --git a/internal/test/externalref/packageB/doc.go b/internal/test/externalref/packageB/doc.go
new file mode 100644
index 0000000..4d28c8c
--- /dev/null
+++ b/internal/test/externalref/packageB/doc.go
@@ -0,0 +1,3 @@
+package packageB
+
+//go:generate go run github.com/deepmap/oapi-codegen/cmd/oapi-codegen -generate types,skip-prune --package=packageB -o externalref.gen.go spec.yaml
diff --git a/internal/test/externalref/packageB/externalref.gen.go b/internal/test/externalref/packageB/externalref.gen.go
new file mode 100644
index 0000000..831b7e8
--- /dev/null
+++ b/internal/test/externalref/packageB/externalref.gen.go
@@ -0,0 +1,9 @@
+// Package packageB provides primitives to interact the openapi HTTP API.
+//
+// Code generated by github.com/deepmap/oapi-codegen DO NOT EDIT.
+package packageB
+
+// ObjectB defines model for ObjectB.
+type ObjectB struct {
+	Name *string `json:"name,omitempty"`
+}
diff --git a/internal/test/externalref/packageB/spec.yaml b/internal/test/externalref/packageB/spec.yaml
new file mode 100644
index 0000000..6f90634
--- /dev/null
+++ b/internal/test/externalref/packageB/spec.yaml
@@ -0,0 +1,6 @@
+components:
+  schemas:
+    ObjectB:
+      properties:
+        name:
+          type: string
diff --git a/internal/test/externalref/spec.yaml b/internal/test/externalref/spec.yaml
new file mode 100644
index 0000000..455ee66
--- /dev/null
+++ b/internal/test/externalref/spec.yaml
@@ -0,0 +1,11 @@
+openapi: "3.0.0"
+info: {}
+paths: {}
+components:
+  schemas:
+    Container:
+      properties:
+        object_a:
+          $ref: ./packageA/spec.yaml#/components/schemas/ObjectA
+        object_b:
+          $ref: ./packageB/spec.yaml#/components/schemas/ObjectB
diff --git a/internal/test/server/server.gen.go b/internal/test/server/server.gen.go
index b4f50e6..9b68f51 100644
--- a/internal/test/server/server.gen.go
+++ b/internal/test/server/server.gen.go
@@ -37,14 +37,15 @@ type EveryTypeOptional struct {
 
 // EveryTypeRequired defines model for EveryTypeRequired.
 type EveryTypeRequired struct {
-	ArrayInlineField     []int              `json:"array_inline_field"`
-	ArrayReferencedField []SomeObject       `json:"array_referenced_field"`
-	BoolField            bool               `json:"bool_field"`
-	ByteField            []byte             `json:"byte_field"`
-	DateField            openapi_types.Date `json:"date_field"`
-	DateTimeField        time.Time          `json:"date_time_field"`
-	DoubleField          float64            `json:"double_field"`
-	FloatField           float32            `json:"float_field"`
+	ArrayInlineField     []int                `json:"array_inline_field"`
+	ArrayReferencedField []SomeObject         `json:"array_referenced_field"`
+	BoolField            bool                 `json:"bool_field"`
+	ByteField            []byte               `json:"byte_field"`
+	DateField            openapi_types.Date   `json:"date_field"`
+	DateTimeField        time.Time            `json:"date_time_field"`
+	DoubleField          float64              `json:"double_field"`
+	EmailField           *openapi_types.Email `json:"email_field,omitempty"`
+	FloatField           float32              `json:"float_field"`
 	InlineObjectField    struct {
 		Name   string `json:"name"`
 		Number int    `json:"number"`
diff --git a/internal/test/test-schema.yaml b/internal/test/test-schema.yaml
index a2fbf31..cc34a9f 100644
--- a/internal/test/test-schema.yaml
+++ b/internal/test/test-schema.yaml
@@ -248,6 +248,9 @@ components:
           type: boolean
         string_field:
           type: string
+        email_field:
+          type: string
+          format: email
         date_field:
           type: string
           format: date
diff --git a/pkg/middleware/oapi_validate_test.go b/pkg/middleware/oapi_validate_test.go
index 50b9e2c..b5952c4 100644
--- a/pkg/middleware/oapi_validate_test.go
+++ b/pkg/middleware/oapi_validate_test.go
@@ -27,6 +27,7 @@ import (
 	"github.com/labstack/echo/v4"
 	echomiddleware "github.com/labstack/echo/v4/middleware"
 	"github.com/stretchr/testify/assert"
+	"github.com/stretchr/testify/require"
 )
 
 var testSchema = `openapi: "3.0.0"
@@ -48,6 +49,7 @@ paths:
             maximum: 100
       responses:
         '200':
+            description: success
             content:
               application/json:
                 schema:
@@ -116,7 +118,7 @@ func doPost(t *testing.T, e *echo.Echo, url string, jsonBody interface{}) *httpt
 
 func TestOapiRequestValidator(t *testing.T) {
 	swagger, err := openapi3.NewSwaggerLoader().LoadSwaggerFromData([]byte(testSchema))
-	assert.NoError(t, err, "Error initializing swagger")
+	require.NoError(t, err, "Error initializing swagger")
 
 	// Create a new echo router
 	e := echo.New()
diff --git a/pkg/types/email_test.go b/pkg/types/email_test.go
new file mode 100644
index 0000000..69fb975
--- /dev/null
+++ b/pkg/types/email_test.go
@@ -0,0 +1,31 @@
+package types
+
+import (
+	"encoding/json"
+	"testing"
+
+	"github.com/stretchr/testify/assert"
+)
+
+func TestEmail_MarshalJSON(t *testing.T) {
+	testEmail := "gaben@valvesoftware.com"
+	b := struct {
+		EmailField Email `json:"email"`
+	}{
+		EmailField: Email(testEmail),
+	}
+	jsonBytes, err := json.Marshal(b)
+	assert.NoError(t, err)
+	assert.JSONEq(t, `{"email":"gaben@valvesoftware.com"}`, string(jsonBytes))
+}
+
+func TestEmail_UnmarshalJSON(t *testing.T) {
+	testEmail := Email("gaben@valvesoftware.com")
+	jsonStr := `{"email":"gaben@valvesoftware.com"}`
+	b := struct {
+		EmailField Email `json:"email"`
+	}{}
+	err := json.Unmarshal([]byte(jsonStr), &b)
+	assert.NoError(t, err)
+	assert.Equal(t, testEmail, b.EmailField)
+}
diff --git a/pkg/util/inputmapping_test.go b/pkg/util/inputmapping_test.go
new file mode 100644
index 0000000..4310463
--- /dev/null
+++ b/pkg/util/inputmapping_test.go
@@ -0,0 +1,62 @@
+package util
+
+import (
+	"testing"
+
+	"github.com/stretchr/testify/assert"
+	"github.com/stretchr/testify/require"
+)
+
+func TestParseInputMapping(t *testing.T) {
+	var src string
+	var expected, parsed map[string]string
+	var err error
+
+	src = "key1:value1,key2:value2"
+	expected = map[string]string{"key1": "value1", "key2":"value2"}
+	parsed, err = ParseCommandlineMap(src)
+	require.NoError(t, err)
+	assert.Equal(t, expected, parsed)
+
+	src = `key1:"value1,value2",key2:value3`
+	expected = map[string]string{"key1": "value1,value2", "key2":"value3"}
+	parsed, err = ParseCommandlineMap(src)
+	require.NoError(t, err)
+	assert.Equal(t, expected, parsed)
+
+	src = `key1:"value1,value2,key2:value3"`
+	expected = map[string]string{"key1": "value1,value2,key2:value3"}
+	parsed, err = ParseCommandlineMap(src)
+	require.NoError(t, err)
+	assert.Equal(t, expected, parsed)
+
+	src = `"key1,key2":value1`
+	expected = map[string]string{"key1,key2": "value1"}
+	parsed, err = ParseCommandlineMap(src)
+	require.NoError(t, err)
+	assert.Equal(t, expected, parsed)
+}
+
+func TestSplitString(t *testing.T) {
+	var src string
+	var expected, result []string
+	var err error
+
+	src = "1,2,3"
+	expected = []string{"1", "2", "3"}
+	result = splitString(src, ',')
+	require.NoError(t, err)
+	assert.Equal(t, expected, result)
+
+	src = `"1,2",3`
+	expected = []string{`"1,2"`,"3"}
+	result = splitString(src, ',')
+	require.NoError(t, err)
+	assert.Equal(t, expected, result)
+
+	src = `1,"2,3",`
+	expected = []string{"1",`"2,3"`, ""}
+	result = splitString(src, ',')
+	require.NoError(t, err)
+	assert.Equal(t, expected, result)
+}