Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

API docs reference resolution #1442

Merged
merged 17 commits into from
Dec 20, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
17 commits
Select commit Hold shift + click to select a range
3b6e6cb
Add OpenAPI 3.1 references test class and update router setup
t-burch Nov 25, 2024
93a5da0
Add JSON serialization for OpenAPI records in OpenAPIPublisherInterce…
t-burch Nov 26, 2024
351a06d
Merge branch 'master' into #1360-api-docs-reference-resolution
t-burch Nov 26, 2024
1ff00ed
Refactor OpenAPI record creation and YAML response handling
t-burch Nov 26, 2024
fc86d2d
Add OpenAPI YAML reference and update JSON serialization in OpenAPIRe…
t-burch Nov 26, 2024
7c1164b
Merge branch 'master' into #1360-api-docs-reference-resolution
t-burch Dec 19, 2024
1713ebc
Refactor OpenAPIRecord creation to streamline parameters in OpenAPIRe…
t-burch Dec 19, 2024
f678606
Refactor OpenAPIRecord creation to use OpenAPI objects directly
t-burch Dec 19, 2024
573ecce
Add tests for Swagger 2 conversion notices and enhance OpenAPIRecordF…
t-burch Dec 20, 2024
2c7706d
Refactor OpenAPIRecordFactory to improve resource handling
t-burch Dec 20, 2024
d0595f8
Refactor OpenAPIRecord creation to reduce duplicate calls and improve…
t-burch Dec 20, 2024
b7ae91f
Update the note in the OpenAPI description to clarify that the descri…
t-burch Dec 20, 2024
fbf7373
Refactor OpenAPI handling by removing Swagger 2 support and updating …
t-burch Dec 20, 2024
e1474c3
Tests
predic8 Dec 20, 2024
3eb9e03
Tests
predic8 Dec 20, 2024
8b97035
Refactor OpenAPIRecordFactory to streamline OpenAPIRecord creation
t-burch Dec 20, 2024
2c8424f
Merge branch 'master' into #1360-api-docs-reference-resolution
t-burch Dec 20, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,6 @@
import static com.predic8.membrane.core.interceptor.Outcome.CONTINUE;
import static com.predic8.membrane.core.interceptor.Outcome.RETURN;
import static com.predic8.membrane.core.openapi.serviceproxy.APIProxy.*;
import static com.predic8.membrane.core.openapi.util.OpenAPIUtil.parseSwaggersInfoServer;
import static com.predic8.membrane.core.openapi.util.UriUtil.getUrlWithoutPath;
import static com.predic8.membrane.core.openapi.util.Utils.*;
import static com.predic8.membrane.core.openapi.validators.ValidationErrors.Direction.REQUEST;
Expand Down Expand Up @@ -229,11 +228,6 @@ protected void setDestinationsFromOpenAPI(OpenAPIRecord rec, Exchange exc) {

private static URL getServerUrlFromOpenAPI(OpenAPIRecord rec, Server server) {
try {
if (rec.isVersion2()) {
return new URL(parseSwaggersInfoServer(server.getUrl()).getUrl());
}

// OpenAPI 3 or newer
return new URL(server.getUrl());
} catch (Exception e) {
throw new RuntimeException("Cannot parse server address from OpenAPI " + server.getUrl());
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@
import com.predic8.membrane.core.interceptor.*;
import com.predic8.membrane.core.util.*;
import groovy.text.*;
import io.swagger.v3.core.util.Json31;
import io.swagger.v3.oas.models.*;
import io.swagger.v3.parser.*;
import org.slf4j.*;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,8 +21,10 @@
import com.predic8.membrane.core.util.*;
import io.swagger.v3.oas.models.*;

import java.io.IOException;
import java.net.*;

import static com.predic8.membrane.core.openapi.serviceproxy.OpenAPIRecordFactory.convert2Json;
import static com.predic8.membrane.core.openapi.util.OpenAPIUtil.*;

public class OpenAPIRecord {
Expand All @@ -43,7 +45,7 @@ public class OpenAPIRecord {
OpenAPISpec spec;

/**
* Version of the OpenAPI standard e.g. 2.0, 3.0.1
* Version of the OpenAPI standard e.g. 3.0.1, 3.1.0
*/
String version;

Expand All @@ -52,29 +54,11 @@ public class OpenAPIRecord {
*/
public OpenAPIRecord() {}

public OpenAPIRecord(OpenAPI api, JsonNode node, OpenAPISpec spec) {
public OpenAPIRecord(OpenAPI api, OpenAPISpec spec) throws IOException {
this.api = api;
this.node = node;
this.node = convert2Json(api);
this.spec = spec;

// If used without a node. Version is read from JSON cause JSON
// supports any version number like 3.1.0 or 3.0.9. For
// getSpecVerison() there are just a number of enum constants.
if (node != null) {
this.version = getOpenAPIVersion(node);
} else {
this.version = api.getSpecVersion().name();
}


}

public boolean isVersion2() {
return version.startsWith("2");
}

public boolean isVersion3() {
return version.startsWith("3");
this.version = api.getSpecVersion().name();
}

public JsonNode rewriteOpenAPI(Exchange exc, URIFactory uriFactory) throws URISyntaxException {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@
import com.predic8.membrane.core.resolver.*;
import com.predic8.membrane.core.util.*;
import io.swagger.parser.*;
import io.swagger.v3.core.util.Json31;
import io.swagger.v3.oas.models.*;
import io.swagger.v3.parser.*;
import io.swagger.v3.parser.core.models.*;
Expand All @@ -43,7 +44,7 @@ public class OpenAPIRecordFactory {

private static final Logger log = LoggerFactory.getLogger(OpenAPIRecordFactory.class.getName());

private final ObjectMapper omYaml = ObjectMapperFactory.createYaml();
private static final ObjectMapper omYaml = ObjectMapperFactory.createYaml();

private final Router router;

Expand Down Expand Up @@ -139,36 +140,68 @@ String getUniqueId(Map<String, OpenAPIRecord> apiRecords, OpenAPIRecord rec) {
}

private OpenAPIRecord create(OpenAPISpec spec) throws IOException {
OpenAPIRecord record = new OpenAPIRecord(getOpenAPI( spec), getSpec( spec), spec);
OpenAPIRecord record = new OpenAPIRecord(getOpenAPI(spec), spec);
setExtensionOnAPI(spec, record.api);
return record;
}

private OpenAPIRecord create(OpenAPISpec spec, File file) throws IOException {
OpenAPIRecord record = new OpenAPIRecord(parseFileAsOpenAPI(file), getSpec(file), spec);
OpenAPIRecord record = new OpenAPIRecord(parseFileAsOpenAPI(file), spec);
setExtensionOnAPI(spec, record.api);
return record;
}

private OpenAPI getOpenAPI( OpenAPISpec spec) {
String path = resolve(spec.location);
public static JsonNode convert2Json(OpenAPI api) throws IOException {
return omYaml.readTree(Json31.mapper().writeValueAsBytes(api));
}

OpenAPI openAPI = new OpenAPIParser().readLocation( path,
null, getParseOptions()).getOpenAPI();
private OpenAPI getOpenAPI(OpenAPISpec spec) {
String path = resolve(spec.location);
try {
JsonNode node = omYaml.readTree(getInputStreamForLocation(spec.location));
OpenAPI openAPI = new OpenAPIParser().readLocation(path, null, getParseOptions()).getOpenAPI();

if (openAPI != null)
addConversionNoticeIfSwagger2(openAPI, node);
return openAPI;
} catch (IOException e) {
throw new OpenAPIParsingException("Could not read OpenAPI file: " + e.getMessage(), path);
}
}

throw new OpenAPIParsingException("Could not read and parse OpenAPI.", path); // Is handled and turned into a nice Exception further up
private InputStream getInputStreamForLocation(String location) throws ResourceRetrievalException {
return router.getResolverMap().resolve(ResolverMap.combine(router.getBaseLocation(), location));
}

private String resolve(String filepath) {
return ResolverMap.combine(router.getBaseLocation(), filepath);
private OpenAPI parseFileAsOpenAPI(File oaFile) {
try {
JsonNode node = omYaml.readTree(oaFile);
OpenAPI api = new OpenAPIParser().readContents(
readInputStream(new FileInputStream(oaFile)),
null,
getParseOptions()
).getOpenAPI();

addConversionNoticeIfSwagger2(api, node);
return api;
} catch (IOException e) {
throw new OpenAPIParsingException("Could not read OpenAPI file: " + e.getMessage(), oaFile.getPath());
}
}

private OpenAPI parseFileAsOpenAPI(File oaFile) throws FileNotFoundException {
return new OpenAPIParser().readContents(readInputStream(new FileInputStream(oaFile)),
null, getParseOptions()).getOpenAPI();
private void addConversionNoticeIfSwagger2(OpenAPI api, JsonNode node) {
if (!isSwagger2(node) || api.getInfo() == null) {
return;
}

StringBuilder builder = new StringBuilder();
builder.append(api.getInfo().getDescription());
if (api.getInfo().getDescription() != null) builder.append("\n\n");
builder.append("***Note:*** *This OpenAPI description was converted from Swagger 2 to OAS 3 by Membrane API Gateway!*");
api.getInfo().setDescription(builder.toString());
}

private String resolve(String filepath) {
return ResolverMap.combine(router.getBaseLocation(), filepath);
}

private static @NotNull ParseOptions getParseOptions() {
Expand All @@ -181,18 +214,6 @@ private OpenAPI parseFileAsOpenAPI(File oaFile) throws FileNotFoundException {
return parseOptions;
}

private InputStream getInputStreamForLocation(String location) throws ResourceRetrievalException {
return router.getResolverMap().resolve(ResolverMap.combine(router.getBaseLocation(), location));
}

private JsonNode getSpec( OpenAPISpec spec) throws IOException {
return omYaml.readTree(getInputStreamForLocation( spec.location));
}

private JsonNode getSpec(File file) throws IOException {
return omYaml.readTree(file);
}

private void setExtensionOnAPI(OpenAPISpec spec, OpenAPI api) {
if (api.getExtensions() == null) {
api.setExtensions(new HashMap<>());
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,17 +16,20 @@

package com.predic8.membrane.core.openapi.serviceproxy;

import com.fasterxml.jackson.databind.*;
import com.fasterxml.jackson.databind.node.*;
import com.predic8.membrane.annot.*;
import com.predic8.membrane.core.exchange.*;
import com.predic8.membrane.core.openapi.util.*;
import com.predic8.membrane.core.util.*;
import org.slf4j.*;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.node.ArrayNode;
import com.fasterxml.jackson.databind.node.ObjectNode;
import com.predic8.membrane.annot.MCAttribute;
import com.predic8.membrane.annot.MCElement;
import com.predic8.membrane.core.exchange.Exchange;
import com.predic8.membrane.core.openapi.util.UriUtil;
import com.predic8.membrane.core.util.URIFactory;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.net.*;
import java.net.URISyntaxException;

import static java.util.Objects.*;
import static java.util.Objects.requireNonNullElse;

/**
* @description
Expand All @@ -43,11 +46,7 @@ public class Rewrite {
String basePath;

public JsonNode rewrite(OpenAPIRecord rec, Exchange exc, URIFactory uriFactory) throws URISyntaxException {
if (rec.isVersion3()) {
return rewriteOpenAPI3(exc, uriFactory, rec.node);
}

return rewriteSwagger2(exc, rec.node);
return rewriteOpenAPI3(exc, uriFactory, rec.node);
}

private JsonNode rewriteOpenAPI3(Exchange exc, URIFactory uriFactory, JsonNode node) throws URISyntaxException {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,15 +16,18 @@

package com.predic8.membrane.core.openapi.util;

import com.fasterxml.jackson.databind.*;
import com.predic8.membrane.core.transport.http.*;
import io.swagger.v3.oas.models.*;
import org.slf4j.*;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import io.swagger.v3.core.util.Json31;
import io.swagger.v3.oas.models.OpenAPI;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.util.regex.*;
import java.io.IOException;
import java.util.regex.Pattern;

import static com.predic8.membrane.core.openapi.serviceproxy.APIProxy.*;
import static com.predic8.membrane.core.openapi.util.Utils.*;
import static com.predic8.membrane.core.openapi.serviceproxy.APIProxy.X_MEMBRANE_ID;
import static com.predic8.membrane.core.openapi.util.Utils.normalizeForId;

public class OpenAPIUtil {

Expand All @@ -45,15 +48,6 @@ private static String getVersionSuffix(OpenAPI api) {
return "-v" + api.getInfo().getVersion();
}

public static String getOpenAPIVersion(JsonNode node) {
if (isSwagger2(node)) {
return node.get("swagger").asText();
} else if (isOpenAPI3(node)) {
return node.get("openapi").asText();
}
log.info("Cannot detect OpenAPI version.");
return "?";
}

public static boolean isOpenAPI3(JsonNode node) {
return node.get("openapi") != null && node.get("openapi").asText().startsWith("3");
Expand All @@ -62,21 +56,4 @@ public static boolean isOpenAPI3(JsonNode node) {
public static boolean isSwagger2(JsonNode node) {
return node.get("swagger") != null && node.get("swagger").asText().startsWith("2");
}

/**
* The OpenAPI parser transforms Swagger 2 specs into OpenAPI 3 documents. Swagger has the field host containing
* only host and port. This field is put into OpenAPI 3 info.server field with the pattern "//HOST:PORT/". This
* method parses this string and returns a HostColonPort object.
* @param server String with the pattern //HOST:PORT/
* @return HostColonPort
*/
public static HostColonPort parseSwaggersInfoServer(String server) throws Exception {
Matcher m = hostPortPattern.matcher(server);
if (m.find()) {
String host = m.group(1);
String port = m.group(2);
return new HostColonPort(false,host,Integer.parseInt(port));
}
throw new Exception("Can't parse server string");
}
}
7 changes: 6 additions & 1 deletion core/src/main/resources/openapi/overview.html
Original file line number Diff line number Diff line change
Expand Up @@ -97,7 +97,12 @@ <h1 class="title">APIs</h1>

</td>
<td><%= api.value.api.info.version %></td>
<td><%= api.value.api.openapi %></td>
<td>
<%= api.value.api.openapi %>
<% if (api.value.api.extensions?.'x-original-swagger-version') { %>
(from <%= api.value.api.extensions.'x-original-swagger-version' %>)
<% } %>
</td>
<td><a href="${path}/${api.key}"><%= api.key %></a></td>
</tr>
<% } %>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,18 +13,23 @@
limitations under the License. */
package com.predic8.membrane.core.openapi.serviceproxy;

import com.predic8.membrane.core.*;
import io.swagger.parser.*;
import io.swagger.v3.oas.models.*;
import org.junit.jupiter.api.*;

import java.io.*;
import java.util.*;

import static com.predic8.membrane.core.http.MimeType.*;
import static com.predic8.membrane.core.openapi.util.TestUtils.*;
import static com.predic8.membrane.core.util.FileUtil.*;
import static io.swagger.v3.oas.models.SpecVersion.*;
import com.predic8.membrane.core.Router;
import io.swagger.parser.OpenAPIParser;
import io.swagger.v3.oas.models.OpenAPI;
import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.Test;

import java.io.IOException;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashMap;
import java.util.Map;

import static com.predic8.membrane.core.http.MimeType.APPLICATION_JSON;
import static com.predic8.membrane.core.openapi.util.TestUtils.getResourceAsStream;
import static com.predic8.membrane.core.util.FileUtil.readInputStream;
import static io.swagger.v3.oas.models.SpecVersion.V30;
import static io.swagger.v3.oas.models.SpecVersion.V31;
import static org.junit.jupiter.api.Assertions.*;

class OpenAPIRecordFactoryTest {
Expand Down Expand Up @@ -60,6 +65,29 @@ void readAndParseSwagger2() throws IOException {
assertEquals(V30, rec.api.getSpecVersion());
}

@Test
void swagger2ConversionNoticeAdded() throws IOException {
OpenAPIRecord rec = getOpenAPIRecord("fruitshop-swagger-2.0.json", "fruit-shop-api-swagger-2-v1-0-0");
String description = rec.api.getInfo().getDescription();
assertTrue(description.contains("Membrane API Gateway"));
}

@Test
void swagger2ConversionNoticeAddedWithExistingDescription() throws IOException {
OpenAPIRecord rec = getOpenAPIRecord("fruitshop-swagger-2.0.json", "fruit-shop-api-swagger-2-v1-0-0");
String description = rec.api.getInfo().getDescription();
assertTrue(description.startsWith("This is a showcase"));
assertTrue(description.contains("Membrane API Gateway"));
}

@Test
void openapi3NoConversionNoticeAdded() throws IOException {
OpenAPIRecord rec = getOpenAPIRecord("fruitshop-api-v2-openapi-3.yml", "fruit-shop-api-v2-0-0");
t-burch marked this conversation as resolved.
Show resolved Hide resolved
assertFalse("OpenAPI description was converted to OAS 3 from Swagger 2 by Membrane API Gateway.".contains(
rec.api.getInfo().getDescription()
));
}

@Test
void referencesTest() throws IOException {
OpenAPIRecord rec = factory.create(new ArrayList<>() {{
Expand Down Expand Up @@ -109,7 +137,6 @@ private static Object getMail(OpenAPIRecord rec) {
.getSchema().getProperties().get("email");
}

// @TODO
private static OpenAPIRecord getOpenAPIRecord(String fileName, String id) throws IOException {
return factory.create(new ArrayList<>() {{
add(new OpenAPISpec() {{
Expand Down
Loading
Loading