Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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
35 changes: 4 additions & 31 deletions impl/openapi/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -7,13 +7,6 @@
</parent>
<artifactId>serverlessworkflow-impl-openapi</artifactId>
<name>Serverless Workflow :: Impl :: OpenAPI</name>

<properties>
<version.org.mozilla.rhino>1.8.1</version.org.mozilla.rhino>
<version.org.apache.commons.lang3>3.20.0</version.org.apache.commons.lang3>
<version.commons.codec>1.20.0</version.commons.codec>
</properties>

<dependencies>
<dependency>
<groupId>jakarta.ws.rs</groupId>
Expand All @@ -27,35 +20,15 @@
<groupId>io.serverlessworkflow</groupId>
<artifactId>serverlessworkflow-impl-http</artifactId>
</dependency>
<dependency>
<groupId>io.swagger.parser.v3</groupId>
<artifactId>swagger-parser</artifactId>
<version>${version.io.swagger.parser.v3}</version>
</dependency>

<!-- Swagger Parser brings a few dependencies with CVE, we are breaking them here -->
<!-- Once they upgrade, we can remove -->
<dependency>
<groupId>org.mozilla</groupId>
<artifactId>rhino</artifactId>
<version>${version.org.mozilla.rhino}</version>
</dependency>
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-lang3</artifactId>
<version>${version.org.apache.commons.lang3}</version>
</dependency>
<dependency>
<groupId>commons-codec</groupId>
<artifactId>commons-codec</artifactId>
<version>${version.commons.codec}</version>
</dependency>

<dependency>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter-engine</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>com.fasterxml.jackson.dataformat</groupId>
<artifactId>jackson-dataformat-yaml</artifactId>
</dependency>
<dependency>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter-params</artifactId>
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,179 @@
/*
* Copyright 2020-Present The Serverless Workflow Specification Authors
*
* 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 io.serverlessworkflow.impl.executors.openapi;

import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.node.ArrayNode;
import com.fasterxml.jackson.databind.node.ObjectNode;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Set;

public class OpenAPI {

public enum SwaggerVersion {
SWAGGER_V2,
OPENAPI_V3
}

private final JsonNode root;
private final boolean isSwaggerV2;

public OpenAPI(JsonNode root) {
this.root = Objects.requireNonNull(root, "root cannot be null");
this.isSwaggerV2 = isSwaggerV2();
this.validatePaths();
this.moveRequestInBodyToRequestBody();
}

private void moveRequestInBodyToRequestBody() {
if (!isSwaggerV2) {
return;
}

JsonNode pathsNode = root.get("paths");
if (pathsNode == null || !pathsNode.isObject()) {
return;
}

Iterator<String> pathNames = pathsNode.fieldNames();
pathNames.forEachRemaining(
pathName -> {
JsonNode pathNode = pathsNode.get(pathName);
if (pathNode == null || !pathNode.isObject()) {
return;
}
Iterator<String> methodNames = pathNode.fieldNames();
methodNames.forEachRemaining(
methodName -> {
JsonNode operationNode = pathNode.get(methodName);
if (operationNode != null && operationNode.isObject()) {
processOperationNode((ObjectNode) operationNode);
}
});
});
}

private void processOperationNode(ObjectNode operationNode) {
JsonNode parametersNode = operationNode.get("parameters");
if (parametersNode == null || !parametersNode.isArray()) {
return;
}

ArrayNode originalParameters = (ArrayNode) parametersNode;
ArrayNode filteredParameters = originalParameters.arrayNode();
boolean requestBodyCreated = false;

for (JsonNode parameterNode : originalParameters) {
if (parameterNode == null || !parameterNode.isObject()) {
filteredParameters.add(parameterNode);
continue;
}

JsonNode inNode = parameterNode.get("in");
if (inNode != null && "body".equals(inNode.asText())) {
if (!requestBodyCreated) {
ObjectNode requestBodyNode = operationNode.putObject("requestBody");
ObjectNode contentNode = requestBodyNode.putObject("content");
ObjectNode mediaTypeNode = contentNode.putObject("application/json");
ObjectNode schemaNode = mediaTypeNode.putObject("schema");
JsonNode schemaFromParam = parameterNode.get("schema");
if (schemaFromParam != null && schemaFromParam.isObject()) {
schemaNode.setAll((ObjectNode) schemaFromParam);
}
requestBodyCreated = true;
}
} else {
filteredParameters.add(parameterNode);
}
}

operationNode.set("parameters", filteredParameters);
}

private void validatePaths() {
if (!root.has("paths")) {
throw new IllegalArgumentException("OpenAPI document must contain 'paths' field");
}
}

private boolean isSwaggerV2() {
JsonNode swaggerNode = root.get("swagger");
return swaggerNode != null && swaggerNode.asText().startsWith("2.0");
}

public PathItemInfo findOperationById(String operationId) {
JsonNode paths = root.get("paths");

Set<Map.Entry<String, JsonNode>> properties = paths.properties();

for (Map.Entry<String, JsonNode> path : properties) {
JsonNode pathNode = path.getValue();
Set<Map.Entry<String, JsonNode>> methods = pathNode.properties();
for (Map.Entry<String, JsonNode> method : methods) {
JsonNode operationNode = method.getValue().get("operationId");
if (operationNode != null && operationNode.asText().equals(operationId)) {
return new PathItemInfo(path.getKey(), path.getValue(), method.getKey());
}
}
}
throw new IllegalArgumentException("Operation with ID " + operationId + " not found");
}

public List<String> getServers() {

if (isSwaggerV2) {
if (root.has("host")) {
String host = root.get("host").asText();
String basePath = root.has("basePath") ? root.get("basePath").asText() : "";
String scheme = "http";
if (root.has("schemes")
&& root.get("schemes").isArray()
&& !root.get("schemes").isEmpty()) {
scheme = root.get("schemes").get(0).asText();
}
return List.of(scheme + "://" + host + basePath);
} else {
return List.of();
}
}

return root.has("servers") ? List.of(root.get("servers").findPath("url").asText()) : List.of();
}

public SwaggerVersion getSwaggerVersion() {
return isSwaggerV2 ? SwaggerVersion.SWAGGER_V2 : SwaggerVersion.OPENAPI_V3;
}

public JsonNode resolveSchema(String ref) {
if (!ref.startsWith("#/")) {
throw new IllegalArgumentException("Only local references are supported");
}
String[] parts = ref.substring(2).split("/");
JsonNode currentNode = root;
for (String part : parts) {
currentNode = currentNode.get(part);
if (currentNode == null) {
throw new IllegalArgumentException("Reference " + ref + " could not be resolved");
}
}
return currentNode;
}

public record PathItemInfo(String path, JsonNode operation, String method) {}
}
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
*/
package io.serverlessworkflow.impl.executors.openapi;

import com.fasterxml.jackson.databind.JsonNode;
import io.serverlessworkflow.api.types.ExternalResource;
import io.serverlessworkflow.impl.TaskContext;
import io.serverlessworkflow.impl.WorkflowApplication;
Expand All @@ -24,7 +25,6 @@
import io.serverlessworkflow.impl.executors.http.HttpExecutor;
import io.serverlessworkflow.impl.executors.http.HttpExecutorBuilder;
import io.serverlessworkflow.impl.resources.ResourceLoaderUtils;
import io.swagger.v3.oas.models.media.Schema;
import java.util.Collection;
import java.util.HashMap;
import java.util.HashSet;
Expand Down Expand Up @@ -113,8 +113,8 @@ private void fillHttpBuilder(WorkflowApplication application, OperationDefinitio
if (!missingParams.isEmpty()) {
throw new IllegalArgumentException(
"Missing required OpenAPI parameters for operation '"
+ (operation.getOperation().getOperationId() != null
? operation.getOperation().getOperationId()
+ (operation.getOperation().get("operationId") != null
? operation.getOperation().get("operationId").asText()
: "<unknown>" + "': ")
+ missingParams);
}
Expand All @@ -135,8 +135,9 @@ private void param(
if (origMap.containsKey(name)) {
collectorMap.put(parameter.getName(), origMap.remove(name));
} else if (parameter.getRequired()) {
Schema<?> schema = parameter.getSchema();
Object defaultValue = schema != null ? schema.getDefault() : null;

JsonNode schema = parameter.getSchema();
Object defaultValue = schema != null ? schema.get("default") : null;
if (defaultValue != null) {
collectorMap.put(name, defaultValue);
} else {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
/*
* Copyright 2020-Present The Serverless Workflow Specification Authors
*
* 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 io.serverlessworkflow.impl.executors.openapi;

import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.json.JsonMapper;
import com.fasterxml.jackson.dataformat.yaml.YAMLMapper;
import java.util.Objects;

/**
* Parses OpenAPI content (JSON or YAML) into a {@link OpenAPI} using Jackson.
*
* <p>This class detects JSON if the first non-whitespace character is '{'; otherwise it treats the
* content as YAML.
*/
public final class OpenAPIParser {

private static final ObjectMapper YAML_MAPPER = new YAMLMapper();
private static final ObjectMapper JSON_MAPPER = new JsonMapper();

/**
* Parse the provided OpenAPI content (JSON or YAML) and return a {@link OpenAPI}.
*
* @param content the OpenAPI document content (must not be null or blank)
* @return parsed {@link OpenAPI}
* @throws IllegalArgumentException if content is null/blank or cannot be parsed
*/
public OpenAPI parse(String content) {
Objects.requireNonNull(content, "content must not be null");
String trimmed = content.trim();
if (trimmed.isEmpty()) {
throw new IllegalArgumentException("content must not be blank");
}

ObjectMapper mapper = selectMapper(trimmed);
try {
JsonNode root = mapper.readTree(content);
return new OpenAPI(root);
} catch (Exception e) {
throw new IllegalArgumentException("Failed to parse content", e);
}
}

private ObjectMapper selectMapper(String trimmedContent) {
char first = firstNonWhitespaceChar(trimmedContent);
if (first == '{') {
return JSON_MAPPER;
}
return YAML_MAPPER;
}

private static char firstNonWhitespaceChar(String s) {
for (int i = 0; i < s.length(); i++) {
char c = s.charAt(i);
if (!Character.isWhitespace(c)) {
return c;
}
}
return '\0';
}
}
Loading