Repository: getkin/kin-openapi
Pull request #155 (2019-12-16 04:24:16)

Resolve references using the relative path of the referenced document
References should be resolved using the relative path of the referenced document.

According to the [Reference Object section](https://github.com/OAI/OpenAPI-Specification/blob/master/versions/3.0.2.md#reference-object) in the OpenAPI 3.0.2 spec, reference resolution is accomplished as defined by the [JSON Reference specification](https://tools.ietf.org/id/draft-pbryan-zyp-json-ref-03.html#RFC3986) which states:

> If the URI contained in the JSON Reference value is a relative URI, then the base URI resolution MUST be calculated according to [RFC3986], section 5.2. Resolution is performed relative to the referring document.

Instead of always using the root document’s directory when resolving refs, this PR correctly tracks the directory of each component as they are resolved.


Patch:
diff --git a/openapi3/swagger_loader.go b/openapi3/swagger_loader.go
index 3bdb80d..56ffe09 100644
--- a/openapi3/swagger_loader.go
+++ b/openapi3/swagger_loader.go
@@ -343,7 +343,7 @@ func (swaggerLoader *SwaggerLoader) resolveHeaderRef(swagger *Swagger, component
 	return nil
 }
 
-func (swaggerLoader *SwaggerLoader) resolveParameterRef(swagger *Swagger, component *ParameterRef, path *url.URL) error {
+func (swaggerLoader *SwaggerLoader) resolveParameterRef(swagger *Swagger, component *ParameterRef, documentPath *url.URL) error {
 	// Prevent infinite recursion
 	visited := swaggerLoader.visited
 	if _, isVisited := visited[component]; isVisited {
@@ -353,15 +353,16 @@ func (swaggerLoader *SwaggerLoader) resolveParameterRef(swagger *Swagger, compon
 
 	// Resolve ref
 	const prefix = "#/components/parameters/"
-	if ref := component.Ref; len(ref) > 0 {
+	ref := component.Ref
+	if len(ref) > 0 {
 		if isSingleRefElement(ref) {
 			var param Parameter
-			if err := swaggerLoader.loadSingleElementFromURI(ref, path, &param); err != nil {
+			if err := swaggerLoader.loadSingleElementFromURI(ref, documentPath, &param); err != nil {
 				return err
 			}
 			component.Value = &param
 		} else {
-			components, id, componentPath, err := swaggerLoader.resolveComponent(swagger, ref, prefix, path)
+			components, id, componentPath, err := swaggerLoader.resolveComponent(swagger, ref, prefix, documentPath)
 			if err != nil {
 				return err
 			}
@@ -383,18 +384,24 @@ func (swaggerLoader *SwaggerLoader) resolveParameterRef(swagger *Swagger, compon
 	if value == nil {
 		return nil
 	}
+
+	refDocumentPath, err := referencedDocumentPath(documentPath, ref)
+	if err != nil {
+		return err
+	}
+
 	if value.Content != nil && value.Schema != nil {
 		return errors.New("Cannot contain both schema and content in a parameter")
 	}
 	for _, contentType := range value.Content {
 		if schema := contentType.Schema; schema != nil {
-			if err := swaggerLoader.resolveSchemaRef(swagger, schema, path); err != nil {
+			if err := swaggerLoader.resolveSchemaRef(swagger, schema, refDocumentPath); err != nil {
 				return err
 			}
 		}
 	}
 	if schema := value.Schema; schema != nil {
-		if err := swaggerLoader.resolveSchemaRef(swagger, schema, path); err != nil {
+		if err := swaggerLoader.resolveSchemaRef(swagger, schema, refDocumentPath); err != nil {
 			return err
 		}
 	}
@@ -458,7 +465,7 @@ func (swaggerLoader *SwaggerLoader) resolveRequestBodyRef(swagger *Swagger, comp
 	return nil
 }
 
-func (swaggerLoader *SwaggerLoader) resolveResponseRef(swagger *Swagger, component *ResponseRef, path *url.URL) error {
+func (swaggerLoader *SwaggerLoader) resolveResponseRef(swagger *Swagger, component *ResponseRef, documentPath *url.URL) error {
 	// Prevent infinite recursion
 	visited := swaggerLoader.visited
 	if _, isVisited := visited[component]; isVisited {
@@ -467,18 +474,19 @@ func (swaggerLoader *SwaggerLoader) resolveResponseRef(swagger *Swagger, compone
 	visited[component] = struct{}{}
 
 	// Resolve ref
+	ref := component.Ref
 	const prefix = "#/components/responses/"
-	if ref := component.Ref; len(ref) > 0 {
+	if len(ref) > 0 {
 
 		if isSingleRefElement(ref) {
 			var resp Response
-			if err := swaggerLoader.loadSingleElementFromURI(ref, path, &resp); err != nil {
+			if err := swaggerLoader.loadSingleElementFromURI(ref, documentPath, &resp); err != nil {
 				return err
 			}
 
 			component.Value = &resp
 		} else {
-			components, id, componentPath, err := swaggerLoader.resolveComponent(swagger, ref, prefix, path)
+			components, id, componentPath, err := swaggerLoader.resolveComponent(swagger, ref, prefix, documentPath)
 			if err != nil {
 				return err
 			}
@@ -496,12 +504,17 @@ func (swaggerLoader *SwaggerLoader) resolveResponseRef(swagger *Swagger, compone
 			component.Value = resolved.Value
 		}
 	}
+	refDocumentPath, err := referencedDocumentPath(documentPath, ref)
+	if err != nil {
+		return err
+	}
+
 	value := component.Value
 	if value == nil {
 		return nil
 	}
 	for _, header := range value.Headers {
-		if err := swaggerLoader.resolveHeaderRef(swagger, header, path); err != nil {
+		if err := swaggerLoader.resolveHeaderRef(swagger, header, refDocumentPath); err != nil {
 			return err
 		}
 	}
@@ -510,13 +523,13 @@ func (swaggerLoader *SwaggerLoader) resolveResponseRef(swagger *Swagger, compone
 			continue
 		}
 		for name, example := range contentType.Examples {
-			if err := swaggerLoader.resolveExampleRef(swagger, example, path); err != nil {
+			if err := swaggerLoader.resolveExampleRef(swagger, example, refDocumentPath); err != nil {
 				return err
 			}
 			contentType.Examples[name] = example
 		}
 		if schema := contentType.Schema; schema != nil {
-			if err := swaggerLoader.resolveSchemaRef(swagger, schema, path); err != nil {
+			if err := swaggerLoader.resolveSchemaRef(swagger, schema, refDocumentPath); err != nil {
 				return err
 			}
 			contentType.Schema = schema
@@ -525,7 +538,7 @@ func (swaggerLoader *SwaggerLoader) resolveResponseRef(swagger *Swagger, compone
 	return nil
 }
 
-func (swaggerLoader *SwaggerLoader) resolveSchemaRef(swagger *Swagger, component *SchemaRef, path *url.URL) error {
+func (swaggerLoader *SwaggerLoader) resolveSchemaRef(swagger *Swagger, component *SchemaRef, documentPath *url.URL) error {
 	// Prevent infinite recursion
 	visited := swaggerLoader.visited
 	if _, isVisited := visited[component]; isVisited {
@@ -535,15 +548,16 @@ func (swaggerLoader *SwaggerLoader) resolveSchemaRef(swagger *Swagger, component
 
 	// Resolve ref
 	const prefix = "#/components/schemas/"
-	if ref := component.Ref; len(ref) > 0 {
+	ref := component.Ref
+	if len(ref) > 0 {
 		if isSingleRefElement(ref) {
 			var schema Schema
-			if err := swaggerLoader.loadSingleElementFromURI(ref, path, &schema); err != nil {
+			if err := swaggerLoader.loadSingleElementFromURI(ref, documentPath, &schema); err != nil {
 				return err
 			}
 			component.Value = &schema
 		} else {
-			components, id, componentPath, err := swaggerLoader.resolveComponent(swagger, ref, prefix, path)
+			components, id, componentPath, err := swaggerLoader.resolveComponent(swagger, ref, prefix, documentPath)
 			if err != nil {
 				return err
 			}
@@ -561,6 +575,12 @@ func (swaggerLoader *SwaggerLoader) resolveSchemaRef(swagger *Swagger, component
 			component.Value = resolved.Value
 		}
 	}
+
+	refDocumentPath, err := referencedDocumentPath(documentPath, ref)
+	if err != nil {
+		return err
+	}
+
 	value := component.Value
 	if value == nil {
 		return nil
@@ -568,37 +588,37 @@ func (swaggerLoader *SwaggerLoader) resolveSchemaRef(swagger *Swagger, component
 
 	// ResolveRefs referred schemas
 	if v := value.Items; v != nil {
-		if err := swaggerLoader.resolveSchemaRef(swagger, v, path); err != nil {
+		if err := swaggerLoader.resolveSchemaRef(swagger, v, refDocumentPath); err != nil {
 			return err
 		}
 	}
 	for _, v := range value.Properties {
-		if err := swaggerLoader.resolveSchemaRef(swagger, v, path); err != nil {
+		if err := swaggerLoader.resolveSchemaRef(swagger, v, refDocumentPath); err != nil {
 			return err
 		}
 	}
 	if v := value.AdditionalProperties; v != nil {
-		if err := swaggerLoader.resolveSchemaRef(swagger, v, path); err != nil {
+		if err := swaggerLoader.resolveSchemaRef(swagger, v, refDocumentPath); err != nil {
 			return err
 		}
 	}
 	if v := value.Not; v != nil {
-		if err := swaggerLoader.resolveSchemaRef(swagger, v, path); err != nil {
+		if err := swaggerLoader.resolveSchemaRef(swagger, v, refDocumentPath); err != nil {
 			return err
 		}
 	}
 	for _, v := range value.AllOf {
-		if err := swaggerLoader.resolveSchemaRef(swagger, v, path); err != nil {
+		if err := swaggerLoader.resolveSchemaRef(swagger, v, refDocumentPath); err != nil {
 			return err
 		}
 	}
 	for _, v := range value.AnyOf {
-		if err := swaggerLoader.resolveSchemaRef(swagger, v, path); err != nil {
+		if err := swaggerLoader.resolveSchemaRef(swagger, v, refDocumentPath); err != nil {
 			return err
 		}
 	}
 	for _, v := range value.OneOf {
-		if err := swaggerLoader.resolveSchemaRef(swagger, v, path); err != nil {
+		if err := swaggerLoader.resolveSchemaRef(swagger, v, refDocumentPath); err != nil {
 			return err
 		}
 	}
@@ -685,12 +705,12 @@ func (swaggerLoader *SwaggerLoader) resolveExampleRef(swagger *Swagger, componen
 	return nil
 }
 
-func (swaggerLoader *SwaggerLoader) resolvePathItemRef(swagger *Swagger, entrypoint string, pathItem *PathItem, path *url.URL) (err error) {
+func (swaggerLoader *SwaggerLoader) resolvePathItemRef(swagger *Swagger, entrypoint string, pathItem *PathItem, documentPath *url.URL) (err error) {
 	// Prevent infinite recursion
 	visited := swaggerLoader.visitedFiles
 	key := "_"
-	if path != nil {
-		key = path.EscapedPath()
+	if documentPath != nil {
+		key = documentPath.EscapedPath()
 	}
 	key += entrypoint
 	if _, isVisited := visited[key]; isVisited {
@@ -702,12 +722,12 @@ func (swaggerLoader *SwaggerLoader) resolvePathItemRef(swagger *Swagger, entrypo
 	if ref != "" {
 		if isSingleRefElement(ref) {
 			var p PathItem
-			if err := swaggerLoader.loadSingleElementFromURI(ref, path, &p); err != nil {
+			if err := swaggerLoader.loadSingleElementFromURI(ref, documentPath, &p); err != nil {
 				return err
 			}
 			*pathItem = p
 		} else {
-			if swagger, ref, path, err = swaggerLoader.resolveRefSwagger(swagger, ref, path); err != nil {
+			if swagger, ref, documentPath, err = swaggerLoader.resolveRefSwagger(swagger, ref, documentPath); err != nil {
 				return
 			}
 
@@ -731,24 +751,29 @@ func (swaggerLoader *SwaggerLoader) resolvePathItemRef(swagger *Swagger, entrypo
 		}
 	}
 
+	refDocumentPath, err := referencedDocumentPath(documentPath, ref)
+	if err != nil {
+		return err
+	}
+
 	for _, parameter := range pathItem.Parameters {
-		if err = swaggerLoader.resolveParameterRef(swagger, parameter, path); err != nil {
+		if err = swaggerLoader.resolveParameterRef(swagger, parameter, refDocumentPath); err != nil {
 			return
 		}
 	}
 	for _, operation := range pathItem.Operations() {
 		for _, parameter := range operation.Parameters {
-			if err = swaggerLoader.resolveParameterRef(swagger, parameter, path); err != nil {
+			if err = swaggerLoader.resolveParameterRef(swagger, parameter, refDocumentPath); err != nil {
 				return
 			}
 		}
 		if requestBody := operation.RequestBody; requestBody != nil {
-			if err = swaggerLoader.resolveRequestBodyRef(swagger, requestBody, path); err != nil {
+			if err = swaggerLoader.resolveRequestBodyRef(swagger, requestBody, refDocumentPath); err != nil {
 				return
 			}
 		}
 		for _, response := range operation.Responses {
-			if err = swaggerLoader.resolveResponseRef(swagger, response, path); err != nil {
+			if err = swaggerLoader.resolveResponseRef(swagger, response, refDocumentPath); err != nil {
 				return
 			}
 		}
@@ -760,3 +785,19 @@ func (swaggerLoader *SwaggerLoader) resolvePathItemRef(swagger *Swagger, entrypo
 func unescapeRefString(ref string) string {
 	return strings.Replace(strings.Replace(ref, "~1", "/", -1), "~0", "~", -1)
 }
+
+func referencedDocumentPath(documentPath *url.URL, ref string) (*url.URL, error) {
+	newDocumentPath := documentPath
+	if documentPath != nil {
+		refDirectory, err := url.Parse(path.Dir(ref))
+		if err != nil {
+			return nil, err
+		}
+		joinedDirectory := path.Join(path.Dir(documentPath.String()), refDirectory.String())
+		newDocumentPath, err = url.Parse(joinedDirectory + "/")
+		if err != nil {
+			return nil, err
+		}
+	}
+	return newDocumentPath, nil
+}


Test patch:
diff --git a/openapi3/swagger_loader_relative_refs_test.go b/openapi3/swagger_loader_relative_refs_test.go
index 49ebba1..6a8108f 100644
--- a/openapi3/swagger_loader_relative_refs_test.go
+++ b/openapi3/swagger_loader_relative_refs_test.go
@@ -903,3 +903,46 @@ paths:
   /pets:
     $ref: relativeDocs/CustomTestPath.yml
 `
+
+func TestLoadSpecWithRelativeDocumentRefs2(t *testing.T) {
+	loader := openapi3.NewSwaggerLoader()
+	loader.IsExternalRefsAllowed = true
+	swagger, err := loader.LoadSwaggerFromFile("testdata/relativeDocsUseDocumentPath/openapi/openapi.yml")
+
+	require.NoError(t, err)
+
+	// path in nested directory
+	// check parameter
+	nestedDirPath := swagger.Paths["/pets/{id}"]
+	require.Equal(t, "param", nestedDirPath.Patch.Parameters[0].Value.Name)
+	require.Equal(t, "path", nestedDirPath.Patch.Parameters[0].Value.In)
+	require.Equal(t, true, nestedDirPath.Patch.Parameters[0].Value.Required)
+
+	// check header
+	require.Equal(t, "header", nestedDirPath.Patch.Responses["200"].Value.Headers["X-Rate-Limit-Reset"].Value.Description)
+
+	// check request body
+	require.Equal(t, "example request", nestedDirPath.Patch.RequestBody.Value.Description)
+
+	// check response schema and example
+	require.Equal(t, nestedDirPath.Patch.Responses["200"].Value.Content["application/json"].Schema.Value.Type, "string")
+	expectedExample := "hello"
+	require.Equal(t,  expectedExample, nestedDirPath.Patch.Responses["200"].Value.Content["application/json"].Examples["CustomTestExample"].Value.Value)
+
+	// path in more nested directory
+	// check parameter
+	moreNestedDirPath := swagger.Paths["/pets/{id}/{city}"]
+	require.Equal(t, "param", moreNestedDirPath.Patch.Parameters[0].Value.Name, )
+	require.Equal(t, "path", moreNestedDirPath.Patch.Parameters[0].Value.In)
+	require.Equal(t, true, moreNestedDirPath.Patch.Parameters[0].Value.Required)
+
+	// check header
+	require.Equal(t, "header", nestedDirPath.Patch.Responses["200"].Value.Headers["X-Rate-Limit-Reset"].Value.Description)
+
+	// check request body
+	require.Equal(t, "example request", moreNestedDirPath.Patch.RequestBody.Value.Description)
+
+	// check response schema and example
+	require.Equal(t, "string", moreNestedDirPath.Patch.Responses["200"].Value.Content["application/json"].Schema.Value.Type)
+	require.Equal(t, moreNestedDirPath.Patch.Responses["200"].Value.Content["application/json"].Examples["CustomTestExample"].Value.Value, expectedExample)
+}
diff --git a/openapi3/testdata/relativeDocs/openapi/openapi.yml b/openapi3/testdata/relativeDocs/openapi/openapi.yml
new file mode 100644
index 0000000..b49d608
--- /dev/null
+++ b/openapi3/testdata/relativeDocs/openapi/openapi.yml
@@ -0,0 +1,9 @@
+openapi: 3.0.0
+info:
+  title: ""
+  version: "1.0"
+paths: {}
+components:
+  responses:
+    TestResponse:
+      $ref: responses/nesteddir/CustomTestResponse.yml
diff --git a/openapi3/testdata/relativeDocs/openapi/responses/custom/CustomTestResponse.yml b/openapi3/testdata/relativeDocs/openapi/responses/custom/CustomTestResponse.yml
new file mode 100644
index 0000000..3e22ba4
--- /dev/null
+++ b/openapi3/testdata/relativeDocs/openapi/responses/custom/CustomTestResponse.yml
@@ -0,0 +1,5 @@
+description: description
+content:
+  application/json:
+    schema:
+      $ref: "../../../CustomTestSchema.yml"
diff --git a/openapi3/testdata/relativeDocsUseDocumentPath/CustomTestExample.yml b/openapi3/testdata/relativeDocsUseDocumentPath/CustomTestExample.yml
new file mode 100644
index 0000000..ccd0608
--- /dev/null
+++ b/openapi3/testdata/relativeDocsUseDocumentPath/CustomTestExample.yml
@@ -0,0 +1,2 @@
+summary: An example
+value: hello
\ No newline at end of file
diff --git a/openapi3/testdata/relativeDocsUseDocumentPath/CustomTestHeader.yml b/openapi3/testdata/relativeDocsUseDocumentPath/CustomTestHeader.yml
new file mode 100644
index 0000000..9d12ac3
--- /dev/null
+++ b/openapi3/testdata/relativeDocsUseDocumentPath/CustomTestHeader.yml
@@ -0,0 +1,1 @@
+description: header
\ No newline at end of file
diff --git a/openapi3/testdata/relativeDocsUseDocumentPath/CustomTestParameter.yml b/openapi3/testdata/relativeDocsUseDocumentPath/CustomTestParameter.yml
new file mode 100644
index 0000000..66aec88
--- /dev/null
+++ b/openapi3/testdata/relativeDocsUseDocumentPath/CustomTestParameter.yml
@@ -0,0 +1,3 @@
+name: param
+in: path
+required: true
diff --git a/openapi3/testdata/relativeDocsUseDocumentPath/CustomTestRequestBody.yml b/openapi3/testdata/relativeDocsUseDocumentPath/CustomTestRequestBody.yml
new file mode 100644
index 0000000..521ad36
--- /dev/null
+++ b/openapi3/testdata/relativeDocsUseDocumentPath/CustomTestRequestBody.yml
@@ -0,0 +1,1 @@
+description: example request
\ No newline at end of file
diff --git a/openapi3/testdata/relativeDocsUseDocumentPath/CustomTestResponse.yml b/openapi3/testdata/relativeDocsUseDocumentPath/CustomTestResponse.yml
new file mode 100644
index 0000000..afa4b97
--- /dev/null
+++ b/openapi3/testdata/relativeDocsUseDocumentPath/CustomTestResponse.yml
@@ -0,0 +1,1 @@
+description: description
\ No newline at end of file
diff --git a/openapi3/testdata/relativeDocsUseDocumentPath/CustomTestSchema.yml b/openapi3/testdata/relativeDocsUseDocumentPath/CustomTestSchema.yml
new file mode 100644
index 0000000..2d8bb0e
--- /dev/null
+++ b/openapi3/testdata/relativeDocsUseDocumentPath/CustomTestSchema.yml
@@ -0,0 +1,1 @@
+type: string
\ No newline at end of file
diff --git a/openapi3/testdata/relativeDocsUseDocumentPath/openapi/openapi.yml b/openapi3/testdata/relativeDocsUseDocumentPath/openapi/openapi.yml
new file mode 100644
index 0000000..5b2e2e9
--- /dev/null
+++ b/openapi3/testdata/relativeDocsUseDocumentPath/openapi/openapi.yml
@@ -0,0 +1,10 @@
+openapi: 3.0.0
+info:
+  title: ""
+  version: "1.0"
+paths:
+  /pets/{id}:
+    $ref: "paths/nesteddir/CustomTestPath.yml"
+  /pets/{id}/{city}:
+    $ref: "paths/nesteddir/morenested/CustomTestPath.yml"
+components: {}
diff --git a/openapi3/testdata/relativeDocsUseDocumentPath/openapi/paths/nesteddir/CustomTestPath.yml b/openapi3/testdata/relativeDocsUseDocumentPath/openapi/paths/nesteddir/CustomTestPath.yml
new file mode 100644
index 0000000..45046c4
--- /dev/null
+++ b/openapi3/testdata/relativeDocsUseDocumentPath/openapi/paths/nesteddir/CustomTestPath.yml
@@ -0,0 +1,19 @@
+patch:
+  description: "Modify a pet"
+  parameters:
+    - $ref: "../../../CustomTestParameter.yml"
+  requestBody:
+    $ref: "../../../CustomTestRequestBody.yml"
+  responses:
+    200:
+      description: "success"
+      headers:
+        X-Rate-Limit-Reset:
+          $ref: "../../../CustomTestHeader.yml"
+      content:
+        application/json:
+          schema:
+            $ref: "../../../CustomTestSchema.yml"
+          examples:
+            CustomTestExample:
+              $ref: "../../../CustomTestExample.yml"
diff --git a/openapi3/testdata/relativeDocsUseDocumentPath/openapi/paths/nesteddir/morenested/CustomTestPath.yml b/openapi3/testdata/relativeDocsUseDocumentPath/openapi/paths/nesteddir/morenested/CustomTestPath.yml
new file mode 100644
index 0000000..647852f
--- /dev/null
+++ b/openapi3/testdata/relativeDocsUseDocumentPath/openapi/paths/nesteddir/morenested/CustomTestPath.yml
@@ -0,0 +1,19 @@
+patch:
+  description: "Modify a pet"
+  parameters:
+    - $ref: "../../../../CustomTestParameter.yml"
+  requestBody:
+    $ref: "../../../../CustomTestRequestBody.yml"
+  responses:
+    200:
+      description: "success"
+      headers:
+        X-Rate-Limit-Reset:
+          $ref: "../../../../CustomTestHeader.yml"
+      content:
+        application/json:
+          schema:
+            $ref: "../../../../CustomTestSchema.yml"
+          examples:
+            CustomTestExample:
+              $ref: "../../../../CustomTestExample.yml"
diff --git a/openapi3/testdata/relativeDocsUseDocumentPath/openapi/responses/nesteddir/CustomTestResponse.yml b/openapi3/testdata/relativeDocsUseDocumentPath/openapi/responses/nesteddir/CustomTestResponse.yml
new file mode 100644
index 0000000..ca9c92d
--- /dev/null
+++ b/openapi3/testdata/relativeDocsUseDocumentPath/openapi/responses/nesteddir/CustomTestResponse.yml
@@ -0,0 +1,10 @@
+description: description
+content:
+  application/json:
+    schema:
+      $ref: "../../../CustomTestSchema.yml"
+    example:
+      $ref: "../../../CustomTestExample.yml"
+    headers:
+      X-Rate-Limit-Reset:
+        $ref: "../../../CustomTestHeader.yml"