Repository: swagger-api/swagger-parser
Pull request #1557 (2021-04-26 15:19:39)

fix handling of relative references
This PR includes a fix & testcase for this [issue] (https://github.com/swagger-api/swagger-parser/issues/1459) 


Patch:
diff --git a/modules/swagger-parser-v3/src/main/java/io/swagger/v3/parser/processors/PathsProcessor.java b/modules/swagger-parser-v3/src/main/java/io/swagger/v3/parser/processors/PathsProcessor.java
index 880837b0..04deeced 100644
--- a/modules/swagger-parser-v3/src/main/java/io/swagger/v3/parser/processors/PathsProcessor.java
+++ b/modules/swagger-parser-v3/src/main/java/io/swagger/v3/parser/processors/PathsProcessor.java
@@ -65,7 +65,7 @@ public class PathsProcessor {
                 String pathRef = pathItem.get$ref().split("#")[0];
 
                 if (resolvedPath != null) {
-                    updateLocalRefs(resolvedPath, pathRef);
+                    updateRefs(resolvedPath, pathRef);
                     //we need to put the resolved path into swagger object
                     openAPI.path(pathStr, resolvedPath);
                     pathItem = resolvedPath;
@@ -124,27 +124,27 @@ public class PathsProcessor {
         }
     }
 
-    protected void updateLocalRefs(PathItem path, String pathRef) {
+    protected void updateRefs(PathItem path, String pathRef) {
         if(path.getParameters() != null) {
             List<Parameter> params = path.getParameters();
             for(Parameter param : params) {
-                updateLocalRefs(param, pathRef);
+                updateRefs(param, pathRef);
             }
         }
         List<Operation> ops = path.readOperations();
         for(Operation op : ops) {
             if(op.getParameters() != null) {
                 for (Parameter param : op.getParameters()) {
-                    updateLocalRefs(param, pathRef);
+                    updateRefs(param, pathRef);
                 }
             }
             if(op.getResponses() != null) {
                 for(ApiResponse response : op.getResponses().values()) {
-                    updateLocalRefs(response, pathRef);
+                    updateRefs(response, pathRef);
                 }
             }
             if (op.getRequestBody() != null){
-                updateLocalRefs(op.getRequestBody(),pathRef);
+                updateRefs(op.getRequestBody(),pathRef);
             }
             if (op.getCallbacks() != null){
                 Map<String,Callback> callbacks = op.getCallbacks();
@@ -153,7 +153,7 @@ public class PathsProcessor {
                     if (callback != null) {
                         for(String callbackName : callback.keySet()) {
                             PathItem pathItem = callback.get(callbackName);
-                            updateLocalRefs(pathItem,pathRef);
+                            updateRefs(pathItem,pathRef);
                         }
                     }
                 }
@@ -161,75 +161,67 @@ public class PathsProcessor {
         }
     }
 
-    protected void updateLocalRefs(ApiResponse response, String pathRef) {
+    protected void updateRefs(ApiResponse response, String pathRef) {
         if (response.get$ref() != null){
-            if(isLocalRef(response.get$ref())) {
-                response.set$ref(computeLocalRef(response.get$ref(), pathRef));
-            }
+        	response.set$ref(computeRef(response.get$ref(), pathRef));
         }
         if(response.getContent() != null) {
             Map<String, MediaType> content = response.getContent();
             for (String key: content.keySet()) {
                 MediaType mediaType = content.get(key);
                 if (mediaType.getSchema() != null) {
-                    updateLocalRefs(mediaType.getSchema(), pathRef);
+                    updateRefs(mediaType.getSchema(), pathRef);
                 }
                 Map<String, Example> examples = content.get(key).getExamples();
                 if (examples != null) {
                     for( Example example:examples.values()){
-                        updateLocalRefs(example, pathRef);
+                        updateRefs(example, pathRef);
                     }
                 }
             }
         }
     }
 
-    protected void updateLocalRefs(Example example, String pathRef) {
+    protected void updateRefs(Example example, String pathRef) {
         if(example.get$ref() != null) {
-            if(isLocalRef(example.get$ref())) {
-                example.set$ref(computeLocalRef(example.get$ref(), pathRef));
-            }
+        	example.set$ref(computeRef(example.get$ref(), pathRef));
         }
     }
 
-    protected void updateLocalRefs(Parameter param, String pathRef) {
+    protected void updateRefs(Parameter param, String pathRef) {
         if (param.get$ref() != null){
-            if(isLocalRef(param.get$ref())) {
-                param.set$ref(computeLocalRef(param.get$ref(), pathRef));
-            }
+        	param.set$ref(computeRef(param.get$ref(), pathRef));
         }
         if(param.getSchema() != null) {
-            updateLocalRefs(param.getSchema(), pathRef);
+            updateRefs(param.getSchema(), pathRef);
         }
         if(param.getContent() != null) {
             Map<String, MediaType> content = param.getContent();
             for (String key: content.keySet()) {
                 MediaType mediaType = content.get(key);
                 if (mediaType.getSchema() != null) {
-                    updateLocalRefs(mediaType.getSchema(), pathRef);
+                    updateRefs(mediaType.getSchema(), pathRef);
                 }
             }
         }
 
     }
 
-    protected void updateLocalRefs(RequestBody body, String pathRef) {
+    protected void updateRefs(RequestBody body, String pathRef) {
         if (body.get$ref() != null){
-            if(isLocalRef(body.get$ref())) {
-                body.set$ref(computeLocalRef(body.get$ref(), pathRef));
-            }
+        	body.set$ref(computeRef(body.get$ref(), pathRef));
         }
         if(body.getContent() != null) {
             Map<String, MediaType> content = body.getContent();
             for (String key: content.keySet()) {
                 MediaType mediaType = content.get(key);
                 if (mediaType.getSchema() != null) {
-                    updateLocalRefs(mediaType.getSchema(), pathRef);
+                    updateRefs(mediaType.getSchema(), pathRef);
                 }
                 Map<String, Example> examples = content.get(key).getExamples();
                 if (examples != null) {
                     for (Example example : examples.values()) {
-                        updateLocalRefs(example, pathRef);
+                        updateRefs(example, pathRef);
                     }
                 }
             }
@@ -238,11 +230,9 @@ public class PathsProcessor {
         }
     }
 
-    protected void updateLocalRefs(Schema model, String pathRef) {
+    protected void updateRefs(Schema model, String pathRef) {
         if(model.get$ref() != null) {
-            if(isLocalRef(model.get$ref())) {
-                model.set$ref(computeLocalRef(model.get$ref(), pathRef));
-            }
+        	model.set$ref(computeRef(model.get$ref(), pathRef));
         }
         else if(model.getProperties() != null) {
             // process properties
@@ -251,7 +241,7 @@ public class PathsProcessor {
                 for(String key : properties.keySet()) {
                     Schema property = properties.get(key);
                     if (property != null) {
-                        updateLocalRefs(property, pathRef);
+                        updateRefs(property, pathRef);
                     }
                 }
             }
@@ -260,22 +250,22 @@ public class PathsProcessor {
             ComposedSchema composedSchema = (ComposedSchema) model;
             if (composedSchema.getAllOf() != null) {
                 for (Schema innerModel : composedSchema.getAllOf()) {
-                    updateLocalRefs(innerModel, pathRef);
+                    updateRefs(innerModel, pathRef);
                 }
             }if (composedSchema.getAnyOf() != null) {
                 for(Schema innerModel : composedSchema.getAnyOf()) {
-                    updateLocalRefs(innerModel, pathRef);
+                    updateRefs(innerModel, pathRef);
                 }
             }if (composedSchema.getOneOf() != null) {
                 for (Schema innerModel : composedSchema.getOneOf()) {
-                    updateLocalRefs(innerModel, pathRef);
+                    updateRefs(innerModel, pathRef);
                 }
             }
         }
         else if(model instanceof ArraySchema) {
             ArraySchema arraySchema = (ArraySchema) model;
             if(arraySchema.getItems() != null) {
-                updateLocalRefs(arraySchema.getItems(), pathRef);
+                updateRefs(arraySchema.getItems(), pathRef);
             }
         }
     }
@@ -287,6 +277,27 @@ public class PathsProcessor {
         }
         return false;
     }
+    
+    protected boolean isAbsoluteRef(String ref) {
+    	 if(ref.startsWith("./")) {
+             return true;
+         }
+         return false;
+    }
+    
+    protected String computeRef(String ref, String prefix) {
+    	if(isLocalRef(ref)) return computeLocalRef(ref, prefix);
+    	if(isAbsoluteRef(ref)) return ref;
+    	return computeRelativeRef(ref, prefix);
+    }
+    
+    protected String computeRelativeRef(String ref, String prefix) {
+    	int iIdxOfSlash = prefix.lastIndexOf('/');
+    	if(iIdxOfSlash != -1) {
+    		return prefix.substring(0, iIdxOfSlash+1) + ref;
+    	}
+    	return prefix + ref;
+    }
 
     protected String computeLocalRef(String ref, String prefix) {
         return prefix + ref;


Test patch:
diff --git a/modules/swagger-parser-v3/src/test/java/io/swagger/v3/parser/test/RelativeReferenceTest.java b/modules/swagger-parser-v3/src/test/java/io/swagger/v3/parser/test/RelativeReferenceTest.java
index bbddf204..20a7ce13 100644
--- a/modules/swagger-parser-v3/src/test/java/io/swagger/v3/parser/test/RelativeReferenceTest.java
+++ b/modules/swagger-parser-v3/src/test/java/io/swagger/v3/parser/test/RelativeReferenceTest.java
@@ -1,8 +1,6 @@
 package io.swagger.v3.parser.test;
 
 
-
-
 import io.swagger.v3.oas.models.OpenAPI;
 import io.swagger.v3.oas.models.parameters.RequestBody;
 import io.swagger.v3.parser.OpenAPIV3Parser;
@@ -12,9 +10,15 @@ import io.swagger.v3.parser.core.models.SwaggerParseResult;
 import io.swagger.v3.parser.util.RemoteUrl;
 import mockit.Expectations;
 import mockit.Mocked;
+
+import org.testng.Assert;
 import org.testng.annotations.Test;
 
+import java.io.File;
+import java.net.MalformedURLException;
+import java.util.ArrayList;
 import java.util.Arrays;
+import java.util.HashSet;
 
 import static org.testng.Assert.assertNotNull;
 import static org.testng.Assert.assertTrue;
@@ -134,4 +138,21 @@ public class RelativeReferenceTest {
         OpenAPI swagger = (new OpenAPIV3Parser().readContents(yaml,null, null)).getOpenAPI();
         assertNotNull(swagger.getComponents().getSchemas().get("ID"));
     }
+        
+    @Test
+    public void testResolveRelativePaths() {
+    	ParseOptions options = new ParseOptions();
+        options.setResolve(true);
+        SwaggerParseResult parseResult = new OpenAPIV3Parser().readLocation("/relative-references-example/openapi.yaml", null, options);
+        
+    	Assert.assertNotNull(parseResult.getOpenAPI());
+    	
+    	HashSet<String> validationMessages = new HashSet<>(null != parseResult.getMessages() ? parseResult.getMessages() : new ArrayList<>());
+    	
+    	
+    	//validationMessages.forEach(msg->System.out.println(msg));
+        //OpenAPI specification = parseResult.getOpenAPI();
+    	Assert.assertTrue(validationMessages.isEmpty(), validationMessages.toString());
+    	
+    }
 }
diff --git a/modules/swagger-parser-v3/src/test/resources/relative-references-example/_responses/UnexpectedError.yaml b/modules/swagger-parser-v3/src/test/resources/relative-references-example/_responses/UnexpectedError.yaml
new file mode 100644
index 00000000..09af1409
--- /dev/null
+++ b/modules/swagger-parser-v3/src/test/resources/relative-references-example/_responses/UnexpectedError.yaml
@@ -0,0 +1,5 @@
+description: unexpected error
+content:
+  application/json:
+    schema:
+      $ref : "../_schemas/error.yaml"
diff --git a/modules/swagger-parser-v3/src/test/resources/relative-references-example/_schemas/error.yaml b/modules/swagger-parser-v3/src/test/resources/relative-references-example/_schemas/error.yaml
new file mode 100644
index 00000000..fecef03f
--- /dev/null
+++ b/modules/swagger-parser-v3/src/test/resources/relative-references-example/_schemas/error.yaml
@@ -0,0 +1,9 @@
+type: object
+properties:
+  code:
+    type: string
+  message:
+    type: string
+required:
+  - code
+  - message
diff --git a/modules/swagger-parser-v3/src/test/resources/relative-references-example/openapi.yaml b/modules/swagger-parser-v3/src/test/resources/relative-references-example/openapi.yaml
new file mode 100644
index 00000000..0fa33d7a
--- /dev/null
+++ b/modules/swagger-parser-v3/src/test/resources/relative-references-example/openapi.yaml
@@ -0,0 +1,16 @@
+openapi: 3.0.1
+info:
+  version: 1.0.0
+  title: x
+  description: desc
+  license:
+    name: public domain
+  contact:
+    name: API Support
+    url: http://www.example.com/support
+    email: support@example.com
+paths:
+  /authorize:
+    $ref: 'security/_resources/authorize.yaml'
+  /login:
+    $ref: 'security/_resources/login.yaml'
diff --git a/modules/swagger-parser-v3/src/test/resources/relative-references-example/security/_resources/authorize.yaml b/modules/swagger-parser-v3/src/test/resources/relative-references-example/security/_resources/authorize.yaml
new file mode 100644
index 00000000..3b0a642b
--- /dev/null
+++ b/modules/swagger-parser-v3/src/test/resources/relative-references-example/security/_resources/authorize.yaml
@@ -0,0 +1,15 @@
+post:
+  tags:
+  - Security
+  description: authorize the user
+  summary: authorize the user
+  operationId: authorize.user
+  responses:
+    "200":
+      description: authorize 200 response
+      content:
+        text/plain:
+          schema:
+            type: string
+    default:
+      $ref: '../../_responses/UnexpectedError.yaml'
diff --git a/modules/swagger-parser-v3/src/test/resources/relative-references-example/security/_resources/login.yaml b/modules/swagger-parser-v3/src/test/resources/relative-references-example/security/_resources/login.yaml
new file mode 100644
index 00000000..7e576f9a
--- /dev/null
+++ b/modules/swagger-parser-v3/src/test/resources/relative-references-example/security/_resources/login.yaml
@@ -0,0 +1,24 @@
+post:
+  tags:
+  - Security
+  description: login
+  summary: login
+  operationId: login
+  parameters: []
+  requestBody:
+    content:
+      application/json:
+        schema:
+          $ref: '../_schemas/usernamepasswordcredentials.yaml'
+      application/x-www-form-urlencoded:
+        schema:
+          $ref: '../_schemas/usernamepasswordcredentials.yaml'
+    required: true
+  responses:
+    "200":
+      description: login 200 response
+      content:
+        application/json:
+          schema:
+            type: object
+  security: []
diff --git a/modules/swagger-parser-v3/src/test/resources/relative-references-example/security/_schemas/usernamepasswordcredentials.yaml b/modules/swagger-parser-v3/src/test/resources/relative-references-example/security/_schemas/usernamepasswordcredentials.yaml
new file mode 100644
index 00000000..18f08f32
--- /dev/null
+++ b/modules/swagger-parser-v3/src/test/resources/relative-references-example/security/_schemas/usernamepasswordcredentials.yaml
@@ -0,0 +1,11 @@
+required:
+- password
+- username
+type: object
+properties:
+  password:
+    minLength: 1
+    type: string
+  username:
+    minLength: 1
+    type: string