Repository: deepmap/oapi-codegen
Pull request #804 (2022-10-21 12:57:23)

Rewrite imports parsing (issue #760)
Close https://github.com/deepmap/oapi-codegen/issues/760


Patch:
diff --git a/pkg/codegen/codegen.go b/pkg/codegen/codegen.go
index 7d03752..308d1cd 100644
--- a/pkg/codegen/codegen.go
+++ b/pkg/codegen/codegen.go
@@ -138,8 +138,12 @@ func Generate(spec *openapi3.T, opts Configuration) (string, error) {
 		return "", fmt.Errorf("error creating operation definitions: %w", err)
 	}
 
+	xGoTypeImports, err := OperationImports(ops)
+	if err != nil {
+		return "", fmt.Errorf("error getting operation imports: %w", err)
+	}
+
 	var typeDefinitions, constantDefinitions string
-	var xGoTypeImports map[string]goImport
 	if opts.Generate.Models {
 		typeDefinitions, err = GenerateTypeDefinitions(t, spec, ops, opts.OutputOptions.ExcludeSchemas)
 		if err != nil {
@@ -151,10 +155,11 @@ func Generate(spec *openapi3.T, opts Configuration) (string, error) {
 			return "", fmt.Errorf("error generating constants: %w", err)
 		}
 
-		xGoTypeImports, err = GetTypeDefinitionsImports(spec, opts.OutputOptions.ExcludeSchemas)
+		imprts, err := GetTypeDefinitionsImports(spec, opts.OutputOptions.ExcludeSchemas)
 		if err != nil {
 			return "", fmt.Errorf("error getting type definition imports: %w", err)
 		}
+		MergeImports(xGoTypeImports, imprts)
 	}
 
 	var echoServerOut string
@@ -794,6 +799,60 @@ func LoadTemplates(src embed.FS, t *template.Template) error {
 	})
 }
 
+func OperationSchemaImports(s *Schema) (map[string]goImport, error) {
+	res := map[string]goImport{}
+
+	for _, p := range s.Properties {
+		imprts, err := GoSchemaImports(&openapi3.SchemaRef{Value: p.Schema.OAPISchema})
+		if err != nil {
+			return nil, err
+		}
+		MergeImports(res, imprts)
+	}
+
+	imprts, err := GoSchemaImports(&openapi3.SchemaRef{Value: s.OAPISchema})
+	if err != nil {
+		return nil, err
+	}
+	MergeImports(res, imprts)
+	return res, nil
+}
+
+func OperationImports(ops []OperationDefinition) (map[string]goImport, error) {
+	res := map[string]goImport{}
+	for _, op := range ops {
+		for _, pd := range [][]ParameterDefinition{op.PathParams, op.QueryParams} {
+			for _, p := range pd {
+				imprts, err := OperationSchemaImports(&p.Schema)
+				if err != nil {
+					return nil, err
+				}
+				MergeImports(res, imprts)
+			}
+		}
+
+		for _, b := range op.Bodies {
+			imprts, err := OperationSchemaImports(&b.Schema)
+			if err != nil {
+				return nil, err
+			}
+			MergeImports(res, imprts)
+		}
+
+		for _, b := range op.Responses {
+			for _, c := range b.Contents {
+				imprts, err := OperationSchemaImports(&c.Schema)
+				if err != nil {
+					return nil, err
+				}
+				MergeImports(res, imprts)
+			}
+		}
+
+	}
+	return res, nil
+}
+
 func GetTypeDefinitionsImports(swagger *openapi3.T, excludeSchemas []string) (map[string]goImport, error) {
 	res := map[string]goImport{}
 	schemaImports, err := GetSchemaImports(swagger.Components.Schemas, excludeSchemas)
@@ -811,9 +870,48 @@ func GetTypeDefinitionsImports(swagger *openapi3.T, excludeSchemas []string) (ma
 		return nil, err
 	}
 
-	for _, imprts := range []map[string]goImport{schemaImports, reqBodiesImports, responsesImports} {
-		for k, v := range imprts {
-			res[k] = v
+	parametersImports, err := GetParametersImports(swagger.Components.Parameters)
+	if err != nil {
+		return nil, err
+	}
+
+	for _, imprts := range []map[string]goImport{schemaImports, reqBodiesImports, responsesImports, parametersImports} {
+		MergeImports(res, imprts)
+	}
+	return res, nil
+}
+
+func GoSchemaImports(schemas ...*openapi3.SchemaRef) (map[string]goImport, error) {
+	res := map[string]goImport{}
+	for _, sref := range schemas {
+		if sref == nil || sref.Value == nil || IsGoTypeReference(sref.Ref) {
+			return nil, nil
+		}
+		if gi, err := ParseGoImportExtension(sref); err != nil {
+			return nil, err
+		} else {
+			if gi != nil {
+				res[gi.String()] = *gi
+			}
+		}
+		schemaVal := sref.Value
+
+		t := schemaVal.Type
+		switch t {
+		case "", "object":
+			for _, v := range schemaVal.Properties {
+				imprts, err := GoSchemaImports(v)
+				if err != nil {
+					return nil, err
+				}
+				MergeImports(res, imprts)
+			}
+		case "array":
+			imprts, err := GoSchemaImports(schemaVal.Items)
+			if err != nil {
+				return nil, err
+			}
+			MergeImports(res, imprts)
 		}
 	}
 	return res, nil
@@ -825,48 +923,31 @@ func GetSchemaImports(schemas map[string]*openapi3.SchemaRef, excludeSchemas []s
 	for _, schema := range excludeSchemas {
 		excludeSchemasMap[schema] = true
 	}
-	for _, schemaName := range SortedSchemaKeys(schemas) {
+	for schemaName, schema := range schemas {
 		if _, ok := excludeSchemasMap[schemaName]; ok {
 			continue
 		}
-		schema := schemas[schemaName].Value
-
-		if schema == nil || schema.Properties == nil {
-			continue
-		}
 
-		imprts, err := GetImports(schema.Properties)
+		imprts, err := GoSchemaImports(schema)
 		if err != nil {
 			return nil, err
 		}
-
-		for s, gi := range imprts {
-			res[s] = gi
-		}
+		MergeImports(res, imprts)
 	}
 	return res, nil
 }
 
 func GetRequestBodiesImports(bodies map[string]*openapi3.RequestBodyRef) (map[string]goImport, error) {
 	res := map[string]goImport{}
-	for _, requestBodyName := range SortedRequestBodyKeys(bodies) {
-		requestBodyRef := bodies[requestBodyName]
-		response := requestBodyRef.Value
+	for _, r := range bodies {
+		response := r.Value
 		jsonBody, found := response.Content["application/json"]
 		if found {
-			schema := jsonBody.Schema
-			if schema == nil || schema.Value == nil || schema.Value.Properties == nil {
-				continue
-			}
-
-			imprts, err := GetImports(schema.Value.Properties)
+			imprts, err := GoSchemaImports(jsonBody.Schema)
 			if err != nil {
 				return nil, err
 			}
-
-			for s, gi := range imprts {
-				res[s] = gi
-			}
+			MergeImports(res, imprts)
 		}
 	}
 	return res, nil
@@ -874,25 +955,31 @@ func GetRequestBodiesImports(bodies map[string]*openapi3.RequestBodyRef) (map[st
 
 func GetResponsesImports(responses map[string]*openapi3.ResponseRef) (map[string]goImport, error) {
 	res := map[string]goImport{}
-	for _, responseName := range SortedResponsesKeys(responses) {
-		responseOrRef := responses[responseName]
-		response := responseOrRef.Value
+	for _, r := range responses {
+		response := r.Value
 		jsonResponse, found := response.Content["application/json"]
 		if found {
-			schema := jsonResponse.Schema
-			if schema == nil || schema.Value == nil || schema.Value.Properties == nil {
-				continue
-			}
-
-			imprts, err := GetImports(schema.Value.Properties)
+			imprts, err := GoSchemaImports(jsonResponse.Schema)
 			if err != nil {
 				return nil, err
 			}
+			MergeImports(res, imprts)
+		}
+	}
+	return res, nil
+}
 
-			for s, gi := range imprts {
-				res[s] = gi
-			}
+func GetParametersImports(params map[string]*openapi3.ParameterRef) (map[string]goImport, error) {
+	res := map[string]goImport{}
+	for _, param := range params {
+		if param.Value == nil {
+			continue
+		}
+		imprts, err := GoSchemaImports(param.Value.Schema)
+		if err != nil {
+			return nil, err
 		}
+		MergeImports(res, imprts)
 	}
 	return res, nil
 }
diff --git a/pkg/codegen/extension.go b/pkg/codegen/extension.go
index a9ee43d..5d01c78 100644
--- a/pkg/codegen/extension.go
+++ b/pkg/codegen/extension.go
@@ -6,6 +6,7 @@ import (
 )
 
 const (
+	extPropGoImport     = "x-go-type-import"
 	extPropGoType       = "x-go-type"
 	extGoName           = "x-go-name"
 	extPropGoJsonIgnore = "x-go-json-ignore"
diff --git a/pkg/codegen/utils.go b/pkg/codegen/utils.go
index 0796b33..a31b553 100644
--- a/pkg/codegen/utils.go
+++ b/pkg/codegen/utils.go
@@ -791,27 +791,26 @@ func findSchemaNameByRefPath(refPath string, spec *openapi3.T) (string, error) {
 	return "", nil
 }
 
-func GetImports(dict map[string]*openapi3.SchemaRef) (map[string]goImport, error) {
-	res := map[string]goImport{}
-	for _, v := range dict {
-		if v == nil || v.Value == nil {
-			continue
-		}
+func ParseGoImportExtension(v *openapi3.SchemaRef) (*goImport, error) {
+	if v.Value.Extensions[extPropGoImport] == nil || v.Value.Extensions[extPropGoType] == nil {
+		return nil, nil
+	}
 
-		if v.Value.Extensions["x-go-type-import"] == nil || v.Value.Extensions["x-go-type"] == nil {
-			continue
-		}
-		goTypeImportExt := v.Value.Extensions["x-go-type-import"]
+	goTypeImportExt := v.Value.Extensions[extPropGoImport]
 
-		if raw, ok := goTypeImportExt.(json.RawMessage); ok {
-			gi := goImport{}
-			if err := json.Unmarshal(raw, &gi); err != nil {
-				return nil, err
-			}
-			res[gi.String()] = gi
-		} else {
-			continue
+	if raw, ok := goTypeImportExt.(json.RawMessage); ok {
+		gi := goImport{}
+		if err := json.Unmarshal(raw, &gi); err != nil {
+			return nil, err
 		}
+		return &gi, nil
+	}
+
+	return nil, nil
+}
+
+func MergeImports(dst, src map[string]goImport) {
+	for k, v := range src {
+		dst[k] = v
 	}
-	return res, nil
 }


Test patch:
diff --git a/pkg/codegen/codegen_test.go b/pkg/codegen/codegen_test.go
index 7b6ee26..dfbca99 100644
--- a/pkg/codegen/codegen_test.go
+++ b/pkg/codegen/codegen_test.go
@@ -190,12 +190,14 @@ type GetTestByNameResponse struct {
 	checkLint(t, "test.gen.go", []byte(code))
 }
 
-func TestXGoTypeImport(t *testing.T) {
+func TestGoTypeImport(t *testing.T) {
 	packageName := "api"
 	opts := Configuration{
 		PackageName: packageName,
 		Generate: GenerateOptions{
-			Models: true,
+			EchoServer:   true,
+			Models:       true,
+			EmbeddedSpec: true,
 		},
 	}
 	spec := "test_specs/x-go-type-import-pet.yaml"
@@ -211,23 +213,24 @@ func TestXGoTypeImport(t *testing.T) {
 	_, err = format.Source([]byte(code))
 	assert.NoError(t, err)
 
-	// Check that we have a package:
-	assert.Contains(t, code, "package api")
-
-	// Check import
-	assert.Contains(t, code, `myuuid "github.com/google/uuid"`)
-
-	// Check generated struct
-	assert.Contains(t, code, "type Pet struct {\n\tAge myuuid.UUID `json:\"age\"`\n}")
+	imports := []string{
+		`github.com/CavernaTechnologies/pgext`, // schemas - direct object
+		`myuuid "github.com/google/uuid"`,      // schemas - object
+		`github.com/lib/pq`,                    // schemas - array
+		`github.com/spf13/viper`,               // responses - direct object
+		`golang.org/x/text`,                    // responses - complex object
+		`golang.org/x/email`,                   // requestBodies - in components
+		`github.com/fatih/color`,               // parameters - query
+		`github.com/go-openapi/swag`,           // parameters - path
+		`github.com/jackc/pgtype`,              // direct parameters - path
+		`github.com/mailru/easyjson`,           // direct parameters - query
+		`github.com/subosito/gotenv`,           // direct request body
+	}
 
 	// Check import
-	assert.Contains(t, code, `github.com/CavernaTechnologies/pgext`)
-
-	// Check generated struct
-	assert.Contains(t, code, "type Person struct {\n\tAge pgext.Puint `json:\"age\"`\n}")
-
-	// Check generated struct
-	assert.Contains(t, code, "type Car struct {\n\tAge int `json:\"age\"`\n}")
+	for _, imp := range imports {
+		assert.Contains(t, code, imp)
+	}
 
 	// Make sure the generated code is valid:
 	checkLint(t, "test.gen.go", []byte(code))
diff --git a/pkg/codegen/test_specs/x-go-type-import-pet.yaml b/pkg/codegen/test_specs/x-go-type-import-pet.yaml
index 9df66b5..b236564 100644
--- a/pkg/codegen/test_specs/x-go-type-import-pet.yaml
+++ b/pkg/codegen/test_specs/x-go-type-import-pet.yaml
@@ -14,70 +14,111 @@ info:
 servers:
   - url: http://petstore.swagger.io/api
 paths:
-  /pets/{id}:
+  /schemas/pet-object:
     get:
-      summary: Returns a pet by ID
-      description: Returns a pet based on a single ID
-      operationId: findPetByID
-      parameters:
-        - name: id
-          in: path
-          description: ID of pet to fetch
-          required: true
-          schema:
-            type: integer
-            format: int64
+      description: return pet object
       responses:
         "200":
-          description: pet response
+          description: person response
           content:
             application/json:
               schema:
-                $ref: "#/components/schemas/Pet"
-  /person/{id}:
+                $ref: "#/components/schemas/PetObject"
+  /schemas/age-simple:
     get:
-      summary: Returns a person by ID
-      description: Returns a person based on a single ID
-      operationId: findPersonByID
-      parameters:
-        - name: id
-          in: path
-          description: ID of person to fetch
-          required: true
-          schema:
-            type: integer
-            format: int64
+      description: return pet object
       responses:
         "200":
           description: person response
           content:
             application/json:
               schema:
-                $ref: "#/components/schemas/Person"
-  /car/{id}:
+                $ref: "#/components/schemas/AgeSimple"
+
+  /schemas/schema-with-array:
     get:
-      summary: Returns a car by ID
-      description: Returns a car based on a single ID
-      operationId: findCarByID
-      parameters:
-        - name: id
-          in: path
-          description: ID of car to fetch
-          required: true
-          schema:
-            type: integer
-            format: int64
+      description: return schema object with array
       responses:
         "200":
-          description: car response
+          description: person response
           content:
             application/json:
               schema:
-                $ref: "#/components/schemas/Car"
+                $ref: "#/components/schemas/PetArray"
+
+  /responses/type:
+    get:
+      responses:
+        '200':
+          $ref: '#/components/responses/StringResponse'
+
+  /responses/complex-response:
+    get:
+      responses:
+        '200':
+          $ref: '#/components/responses/ComplexResponse'
+
+  /requests/:
+    post:
+      requestBody:
+        $ref: "#/components/requestBodies/RegisterBody"
+      responses:
+        '200':
+          $ref: '#/components/responses/ComplexResponse'
+
+  /parameters/query:
+    post:
+      parameters:
+        - $ref: "#/components/parameters/Version"
+      requestBody:
+        $ref: "#/components/requestBodies/RegisterBody"
+      responses:
+        '200':
+          $ref: '#/components/responses/ComplexResponse'
+
+  /parameters/{versionPath}:
+    post:
+      parameters:
+        - $ref: "#/components/parameters/VersionPath"
+      requestBody:
+        $ref: "#/components/requestBodies/RegisterBody"
+      responses:
+        '200':
+          $ref: '#/components/responses/ComplexResponse'
+
+  /requests/{path}:
+    post:
+      parameters:
+        - name: version
+          in: query
+          schema:
+            x-go-type: easyjson.Json
+            x-go-type-import:
+              path: github.com/mailru/easyjson
+        - name: path
+          in: path
+          schema:
+            x-go-type: pgtype.Color
+            x-go-type-import:
+              path: github.com/jackc/pgtype
+      requestBody:
+        required: true
+        content:
+          application/json:
+            schema:
+              type: object
+              properties:
+                email:
+                  x-go-type: gotenv.Env
+                  x-go-type-import:
+                    path: github.com/subosito/gotenv
+      responses:
+        '200':
+            $ref: '#/components/responses/ComplexResponse'
 
 components:
   schemas:
-    Pet:
+    PetObject:
       properties:
         age:
           x-go-type: myuuid.UUID
@@ -86,17 +127,67 @@ components:
             name: myuuid
       required:
         - age
-    Person:
-      properties:
-        age:
-          x-go-type: pgext.Puint
-          x-go-type-import:
-            path: github.com/CavernaTechnologies/pgext
-      required:
-        - age
-    Car:
-      properties:
-        age:
-          type: integer
-      required:
-        - age
+    AgeSimple:
+      x-go-type: pgext.Puint
+      x-go-type-import:
+        path: github.com/CavernaTechnologies/pgext
+    PetArray:
+      type: array
+      items:
+        x-go-type: pq.Int
+        x-go-type-import:
+          path: github.com/lib/pq
+
+  responses:
+    StringResponse:
+      description: An image.
+      content:
+        application/json:
+          schema:
+            type: string
+            x-go-type: viper.ID
+            x-go-type-import:
+              path: github.com/spf13/viper
+    ComplexResponse:
+      description: Complex response
+      content:
+        application/json:
+          schema:
+            type: object
+            properties:
+              keys:
+                type: array
+                items:
+                  x-go-type: text.Text
+                  x-go-type-import:
+                    path: golang.org/x/text
+
+  requestBodies:
+    RegisterBody:
+      required: true
+      content:
+        application/json:
+          schema:
+            type: object
+            properties:
+              email:
+                x-go-type: email.Email
+                x-go-type-import:
+                  path: golang.org/x/email
+
+  parameters:
+    Version:
+      name: version
+      in: query
+      schema:
+        x-go-type: color.Color
+        x-go-type-import:
+          path: github.com/fatih/color
+
+    VersionPath:
+      name: versionPath
+      in: path
+      schema:
+        x-go-type: swag.Swag
+        x-go-type-import:
+          path: github.com/go-openapi/swag
\ No newline at end of file
diff --git a/pkg/codegen/utils_test.go b/pkg/codegen/utils_test.go
index 7bbfaff..518e287 100644
--- a/pkg/codegen/utils_test.go
+++ b/pkg/codegen/utils_test.go
@@ -14,8 +14,6 @@
 package codegen
 
 import (
-	"encoding/json"
-	"fmt"
 	"testing"
 
 	"github.com/getkin/kin-openapi/openapi3"
@@ -434,44 +432,3 @@ func TestSchemaNameToTypeName(t *testing.T) {
 		assert.Equal(t, want, SchemaNameToTypeName(in))
 	}
 }
-
-func TestGetImports(t *testing.T) {
-	schemas := map[string]*openapi3.SchemaRef{
-		"age": {
-			Value: &openapi3.Schema{
-				ExtensionProps: openapi3.ExtensionProps{
-					Extensions: map[string]interface{}{
-						"x-go-type-import": json.RawMessage(
-							`{"name": "hello", "path": "github.com/google/uuid"}`,
-						),
-						"x-go-type": json.RawMessage(
-							"hello.UUID",
-						),
-					},
-				},
-			},
-		},
-		"name": {
-			Value: &openapi3.Schema{
-				ExtensionProps: openapi3.ExtensionProps{
-					Extensions: map[string]interface{}{"other-tag": json.RawMessage(
-						`bla`,
-					)},
-				},
-			},
-		},
-		"value": nil,
-	}
-
-	expected := map[string]goImport{
-		fmt.Sprintf("%s %q", "hello", "github.com/google/uuid"): {
-			Name: "hello",
-			Path: "github.com/google/uuid",
-		},
-	}
-
-	res, err := GetImports(schemas)
-
-	assert.NoError(t, err)
-	assert.Equal(t, expected, res)
-}