Skip to content

Commit

Permalink
feat(OpenAPI): add a JSON:API endpoint
Browse files Browse the repository at this point in the history
Signed-off-by: Martin Dünkelmann <[email protected]>
  • Loading branch information
MartinX3 committed Oct 27, 2023
1 parent f5c420b commit f4f5c88
Show file tree
Hide file tree
Showing 5 changed files with 195 additions and 1 deletion.
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ API Gateway for REST, WebSockets and legacy Web Services written in Java. Featur
**OpenAPI:**
* API [Deployment from OpenAPI](https://membrane-api.io/openapi/)
* [Message validation](distribution/examples/openapi/validation-simple) against OpenAPI and JSON Schema
* Delivering a JSON:API compatible list of APIs via an endpoint

**API Security:**
* [JSON Web Tokens](#json-web-tokens)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -91,6 +91,7 @@ private void initOpenAPI() throws IOException, ClassNotFoundException {
basePaths = getOpenAPIMap();
configureBasePaths();

interceptors.add(new JSONAPIPublisherInterceptor(apiRecords));
interceptors.add(new OpenAPIPublisherInterceptor(apiRecords));
interceptors.add(new OpenAPIInterceptor(this));
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,130 @@
/*
* Copyright 2023 predic8 GmbH, www.predic8.com
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

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

import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.ObjectWriter;
import com.fasterxml.jackson.databind.node.ArrayNode;
import com.fasterxml.jackson.databind.node.ObjectNode;
import com.predic8.membrane.core.exchange.Exchange;
import com.predic8.membrane.core.interceptor.AbstractInterceptor;
import com.predic8.membrane.core.interceptor.Outcome;

import java.util.Map;
import java.util.Map.Entry;
import java.util.Set;

import static com.predic8.membrane.core.http.MimeType.APPLICATION_JSON;
import static com.predic8.membrane.core.http.Response.ok;
import static com.predic8.membrane.core.interceptor.Outcome.CONTINUE;
import static com.predic8.membrane.core.interceptor.Outcome.RETURN;
import static com.predic8.membrane.core.util.URLUtil.getHost;
import static java.time.LocalDateTime.now;

public class JSONAPIPublisherInterceptor extends AbstractInterceptor {
public static final String PATH = "/apis.json";
protected final Map<String, OpenAPIRecord> apis;
private final ObjectMapper om = new ObjectMapper();
private final ObjectWriter ow = new ObjectMapper().writerWithDefaultPrettyPrinter();

public JSONAPIPublisherInterceptor(Map<String, OpenAPIRecord> apis) {
this.apis = apis;
name = "JSON:API Publisher";
}

@Override
public String getShortDescription() {
return "Publishes the JSON:API description.";
}

@Override
public Outcome handleRequest(Exchange exc) throws Exception {
if (!exc.getRequest().getUri().startsWith(PATH)) return CONTINUE;

return returnJsonOverview(exc);
}

private Outcome returnJsonOverview(Exchange exc) throws JsonProcessingException {
exc.setResponse(ok().contentType(APPLICATION_JSON).body(ow.writeValueAsBytes(createJsonAPIStructure(exc.getOriginalHostHeader()))).build());

return RETURN;
}

private void addAttributes(ObjectNode node) {
addAttributeTimestamps(node.putObject("timestamps"));
node.put("title", "Membrane API List");
}

private void addAttributeTimestamps(ObjectNode node) {
node.put("created", now().toString());
node.put("modified", now().toString());
}

private void addData(ArrayNode node, String hostHeader) {
apis.forEach((k, v) -> addDataAPI(hostHeader, v, node.addObject()));
}

private void addDataAPI(String hostHeader, OpenAPIRecord value, ObjectNode node) {
addDataAPIAttributes(hostHeader, value, node.putObject("attributes"));
node.put("type", "apis");
}

private void addDataAPIAttributes(String hostHeader, OpenAPIRecord value, ObjectNode node) {
JsonNode infoNode = value.node.get("info");

addDataAPIAttributesBaseURL(node, value.node.get("servers").get(0).get("url").asText(), hostHeader);
addDataAPIAttributesPaths(node.putArray("paths"), value);

node.put("title", infoNode.get("title").asText());
node.put("version", infoNode.get("version").asText());
}

private void addDataAPIAttributesBaseURL(ObjectNode node, String server, String hostHeader) {
node.put("baseURL", server.replace(getHost(server), hostHeader));
}

private void addDataAPIAttributesPaths(ArrayNode node, OpenAPIRecord value) {
value.node.get("paths").properties().forEach(it -> addDataAPIAttributesPathsContent(node, it.getKey(), it.getValue().properties()));
}

private void addDataAPIAttributesPathsContent(ArrayNode node, String key, Set<Entry<String, JsonNode>> entrySet) {
entrySet.stream()
.filter(it -> it.getValue().has("operationId"))
.forEach(it -> addDataAPIAttributesPathsContentHelper(node.addObject(), key, it.getKey().toUpperCase()));
}

private void addDataAPIAttributesPathsContentHelper(ObjectNode node, String url, String type) {
node.put("type", type).put("url", url);
}

private void addLinks(ObjectNode node, String hostHeader) {
node.put("self", "https://%s%s".formatted(hostHeader, PATH));
}

private ObjectNode createJsonAPIStructure(String hostHeader) {
ObjectNode node = om.createObjectNode();

addAttributes(node.putObject("attributes"));
addData(node.putArray("data"), hostHeader);
addLinks(node.putObject("links"), hostHeader);
node.put("version", 1.1); // JSON:API Version

return node;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,9 @@ public class OpenAPIProxyServiceKey extends ServiceProxyKey {
public OpenAPIProxyServiceKey(String ip, String host, int port) {
super(host, "*", null, port, ip);

// Add basePaths of JSONAPIPublisherInterceptor to accept them also
basePaths.add(JSONAPIPublisherInterceptor.PATH);

// Add basePaths of OpenAPIPublisherInterceptor to accept them also
basePaths.add(OpenAPIPublisherInterceptor.PATH); // new path
basePaths.add(OpenAPIPublisherInterceptor.PATH_UI); // "
Expand Down
61 changes: 60 additions & 1 deletion distribution/examples/openapi/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,8 @@ Membrane's **openAPIProxy** offers support for [OpenAPI](https://github.com/OAI/
- Configuration from OpenAPI
- Request and response validation against OpenAPI, including paths, parameters and JSON bodies.
- Rewriting of addresses
- Swagger UI integration
- Swagger UI integration
- Generating and providing a JSON:API compatible list of APIs

This page serves as a reference for the functions and configuration. See also the examples:

Expand Down Expand Up @@ -150,6 +151,64 @@ TLS for incoming and outgoing connections can be configured in the same way as f
</api>
```

# JSON:API endpoint

Calling the `/apis.json` endpoint will provide a JSON:API compatible list of APIs.

As example using
```xml
<api port="2000">
<openapi location="fruitshop-api.yml"/>
<openapi dir="openapi"/>
<openapi location="https://developer.lufthansa.com/swagger/export/21516"/>
<openapi location="https://api.apis.guru/v2/specs/nowpayments.io/1.0.0/openapi.json"/>
<openapi location="https://raw.githubusercontent.com/openai/openai-openapi/master/openapi.yaml"/>
</api>
```
Will create the endpoint `localhost:2000/apis.json`.

Calling it will give you a document following the JSON:API standard:

```json
{
"attributes": {
"timestamps": {
"created": "2020-07-21T12:09:00Z",
"modified": "2020-07-30T10:19:01Z"
},
"title": "Membrane API List"
},
"data": [
{
"attributes": {
"baseURL": "http://localhost:2000/shop/v2",
"paths": [
{
"type": "GET",
"url": "/products"
},
{
"type": "POST",
"url": "/products"
},
{
"type": "GET",
"url": "/products/{pid}"
}
],
"title": "Fruit Shop API",
"version": "2.0"
},
"type": "apis"
},
...
],
"links": {
"self": "http://localhost:2000/apis.json"
},
"version": "1.1"
}
```

# Plugins / Interceptors

Expand Down

0 comments on commit f4f5c88

Please sign in to comment.