Repository: getkin/kin-openapi
Pull request #583 (2022-08-23 04:42:51)

Fix `resolveSchemaRef()` to load correctly an other spec. file referenced by `$ref`
# Fix `resolveSchemaRef()` to load correctly an other spec. file referenced by `$ref`

I can't find contribution guidelines. If you know it, would you let me know it?
At the following case, `resolveSchemaRef()` fails to load other spec. files.

* A schema participate a circular reference.
* The schema has references to component's schema in external spec. files.

## Changes

* Add test case: `TestLoadCircularRefFromFile`
* Change `resolveSchemaRef()` to fix it.

## Cause analysis
`resolveSchemaRef()` miss file path's information during recursive resolving schema reference.
For describe, I'm going use following example.

```directory tree
./testdata
    |- a.yml
    |- b.yml
```

```a.yml
# a.yml
components:
  schemas:
    Foo:
      properties:
        bar:
          $ref: "#/components/schemas/Bar"
        foo2:
          $ref: "b.yml#/components/schemas/Foo"
    Bar:
      properties:
        foo:
          $ref: "#/components/schemas/Foo"
```

```b.yml
# b.yml
components:
  schemas:
    Foo:
      properties:
        id:
          type: string
```

Resolving `Foo`, if process load `foo2` before `bar`, it loads with an order as `Foo` -> `Bar` -> `Foo`.
During 2nd loading `Foo` schema, because `loader.resolveComponent()` set a instance with empty Ref field to `resolved`, the process lost file path information.

Because of golang specification, order of for-loop over map is random, I fail to resolve Foo randomly.

fenollp: Please rebase on top of https://github.com/getkin/kin-openapi/pull/584
masu-mi: @fenollp Thank you for you feedback. I've rebased this PR and update test code to add assertion with `require` package.

Patch:
diff --git a/openapi3/loader.go b/openapi3/loader.go
index e2f131e..c981a33 100644
--- a/openapi3/loader.go
+++ b/openapi3/loader.go
@@ -699,7 +699,8 @@ func (loader *Loader) resolveSchemaRef(doc *T, component *SchemaRef, documentPat
 				return err
 			}
 			component.Value = resolved.Value
-			documentPath = loader.documentPathForRecursiveRef(documentPath, resolved.Ref)
+			foundPath := loader.getResolvedRefPath(ref, &resolved, documentPath, componentPath)
+			documentPath = loader.documentPathForRecursiveRef(documentPath, foundPath)
 		}
 	}
 	value := component.Value
@@ -746,6 +747,22 @@ func (loader *Loader) resolveSchemaRef(doc *T, component *SchemaRef, documentPat
 	return nil
 }
 
+func (loader *Loader) getResolvedRefPath(ref string, resolved *SchemaRef, cur, found *url.URL) string {
+	referencedFilename := strings.Split(ref, "#")[0]
+	if referencedFilename == "" {
+		if cur != nil {
+			return path.Base(cur.Path)
+		}
+		return ""
+	}
+	// ref. to external file
+	if resolved.Ref != "" {
+		return resolved.Ref
+	}
+	// found dest spec. file
+	return path.Dir(found.Path)[len(loader.rootDir):]
+}
+
 func (loader *Loader) resolveSecuritySchemeRef(doc *T, component *SecuritySchemeRef, documentPath *url.URL) (err error) {
 	if component != nil && component.Value != nil {
 		if loader.visitedSecurityScheme == nil {


Test patch:
diff --git a/openapi3/load_cicular_ref_with_external_file_test.go b/openapi3/load_cicular_ref_with_external_file_test.go
new file mode 100644
index 0000000..85b127e
--- /dev/null
+++ b/openapi3/load_cicular_ref_with_external_file_test.go
@@ -0,0 +1,82 @@
+//go:build go1.16
+// +build go1.16
+
+package openapi3_test
+
+import (
+	"embed"
+	"net/url"
+	"testing"
+
+	"github.com/stretchr/testify/require"
+
+	"github.com/getkin/kin-openapi/openapi3"
+)
+
+//go:embed testdata/circularRef/*
+var circularResSpecs embed.FS
+
+func TestLoadCircularRefFromFile(t *testing.T) {
+	loader := openapi3.NewLoader()
+	loader.IsExternalRefsAllowed = true
+	loader.ReadFromURIFunc = func(loader *openapi3.Loader, uri *url.URL) ([]byte, error) {
+		return circularResSpecs.ReadFile(uri.Path)
+	}
+
+	got, err := loader.LoadFromFile("testdata/circularRef/base.yml")
+	if err != nil {
+		t.Error(err)
+	}
+
+	foo := &openapi3.SchemaRef{
+		Ref: "",
+		Value: &openapi3.Schema{
+			ExtensionProps: openapi3.ExtensionProps{Extensions: map[string]interface{}{}},
+			Properties: map[string]*openapi3.SchemaRef{
+				"foo2": { // reference to an external file
+					Ref: "other.yml#/components/schemas/Foo2",
+					Value: &openapi3.Schema{
+						ExtensionProps: openapi3.ExtensionProps{Extensions: map[string]interface{}{}},
+						Properties: map[string]*openapi3.SchemaRef{
+							"id": {
+								Value: &openapi3.Schema{
+									Type:           "string",
+									ExtensionProps: openapi3.ExtensionProps{Extensions: map[string]interface{}{}},
+								}},
+						},
+					},
+				},
+			},
+		},
+	}
+	bar := &openapi3.SchemaRef{
+		Ref: "",
+		Value: &openapi3.Schema{
+			ExtensionProps: openapi3.ExtensionProps{Extensions: map[string]interface{}{}},
+			Properties:     map[string]*openapi3.SchemaRef{},
+		},
+	}
+	// circular reference
+	bar.Value.Properties["foo"] = &openapi3.SchemaRef{Ref: "#/components/schemas/Foo", Value: foo.Value}
+	foo.Value.Properties["bar"] = &openapi3.SchemaRef{Ref: "#/components/schemas/Bar", Value: bar.Value}
+
+	want := &openapi3.T{
+		OpenAPI: "3.0.3",
+		Info: &openapi3.Info{
+			Title:   "Recursive cyclic refs example",
+			Version: "1.0",
+
+			ExtensionProps: openapi3.ExtensionProps{Extensions: map[string]interface{}{}},
+		},
+		Components: openapi3.Components{
+			ExtensionProps: openapi3.ExtensionProps{Extensions: map[string]interface{}{}},
+			Schemas: map[string]*openapi3.SchemaRef{
+				"Foo": foo,
+				"Bar": bar,
+			},
+		},
+		ExtensionProps: openapi3.ExtensionProps{Extensions: map[string]interface{}{}},
+	}
+
+	require.Equal(t, want, got)
+}
diff --git a/openapi3/testdata/circularRef/base.yml b/openapi3/testdata/circularRef/base.yml
new file mode 100644
index 0000000..ff8240e
--- /dev/null
+++ b/openapi3/testdata/circularRef/base.yml
@@ -0,0 +1,16 @@
+openapi: "3.0.3"
+info:
+  title: Recursive cyclic refs example
+  version: "1.0"
+components:
+  schemas:
+    Foo:
+      properties:
+        foo2:
+          $ref: "other.yml#/components/schemas/Foo2"
+        bar:
+          $ref: "#/components/schemas/Bar"
+    Bar:
+      properties:
+        foo:
+          $ref: "#/components/schemas/Foo"
diff --git a/openapi3/testdata/circularRef/other.yml b/openapi3/testdata/circularRef/other.yml
new file mode 100644
index 0000000..29b72d9
--- /dev/null
+++ b/openapi3/testdata/circularRef/other.yml
@@ -0,0 +1,10 @@
+openapi: "3.0.3"
+info:
+  title: Recursive cyclic refs example
+  version: "1.0"
+components:
+  schemas:
+    Foo2:
+      properties:
+        id:
+          type: string