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
33 changes: 3 additions & 30 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.9.0</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 @@ -25,32 +18,12 @@
</dependency>
<dependency>
<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>
<artifactId>serverlessworkflow-api</artifactId>
</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>
<groupId>io.serverlessworkflow</groupId>
<artifactId>serverlessworkflow-impl-http</artifactId>
</dependency>

<dependency>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter-engine</artifactId>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,8 @@
*/
package io.serverlessworkflow.impl.executors.openapi;

import com.fasterxml.jackson.databind.ObjectMapper;
import io.serverlessworkflow.api.WorkflowFormat;
import io.serverlessworkflow.api.types.ExternalResource;
import io.serverlessworkflow.impl.TaskContext;
import io.serverlessworkflow.impl.WorkflowApplication;
Expand All @@ -23,8 +25,10 @@
import io.serverlessworkflow.impl.executors.CallableTask;
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 io.serverlessworkflow.impl.resources.ExternalResourceHandler;
import java.io.IOException;
import java.io.InputStream;
import java.io.UncheckedIOException;
import java.util.Collection;
import java.util.HashMap;
import java.util.HashSet;
Expand Down Expand Up @@ -61,20 +65,15 @@ public CompletableFuture<WorkflowModel> apply(
workflowContext
.definition()
.resourceLoader()
.load(
resource,
ResourceLoaderUtils::readString,
workflowContext,
taskContext,
input));
.load(resource, this::readUnifiedOpenAPI, workflowContext, taskContext, input));

fillHttpBuilder(workflowContext.definition().application(), operationDefinition);
// One executor per operation, even if the document is the same
// Me may refactor this even further to reuse the same executor (since the base URI is the same,
// but the path differs, although some use cases may require different client configurations for
// different paths...)
Collection<HttpExecutor> executors =
operationDefinition.getServers().stream().map(s -> builder.build(s)).toList();
operationDefinition.getServers().stream().map(builder::build).toList();

Iterator<HttpExecutor> iter = executors.iterator();
if (!iter.hasNext()) {
Expand All @@ -97,7 +96,7 @@ private void fillHttpBuilder(WorkflowApplication application, OperationDefinitio

Map<String, Object> bodyParameters = new HashMap<>(parameters);
for (ParameterDefinition parameter : operation.getParameters()) {
switch (parameter.getIn()) {
switch (parameter.in()) {
case "header":
param(parameter, bodyParameters, headersMap, missingParams);
break;
Expand All @@ -113,8 +112,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().operationId() != null
? operation.getOperation().operationId()
: "<unknown>" + "': ")
+ missingParams);
}
Expand All @@ -131,17 +130,27 @@ private void param(
Map<String, Object> origMap,
Map<String, Object> collectorMap,
Set<String> missingParams) {
String name = parameter.getName();
String name = parameter.name();
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;
collectorMap.put(parameter.name(), origMap.remove(name));
} else if (parameter.required()) {

UnifiedOpenAPI.Schema schema = parameter.schema();
Object defaultValue = schema != null ? schema._default() : null;
if (defaultValue != null) {
collectorMap.put(name, defaultValue);
} else {
missingParams.add(name);
}
}
}

private UnifiedOpenAPI readUnifiedOpenAPI(ExternalResourceHandler handler) {
ObjectMapper objectMapper = WorkflowFormat.fromFileName(handler.name()).mapper();
try (InputStream is = handler.open()) {
return objectMapper.readValue(is, UnifiedOpenAPI.class);
} catch (IOException e) {
throw new UncheckedIOException("Error while reading OpenAPI document " + handler.name(), e);
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -15,89 +15,22 @@
*/
package io.serverlessworkflow.impl.executors.openapi;

import io.swagger.parser.OpenAPIParser;
import io.swagger.v3.oas.models.OpenAPI;
import io.swagger.v3.oas.models.Operation;
import io.swagger.v3.oas.models.PathItem;
import io.swagger.v3.parser.core.models.ParseOptions;
import io.swagger.v3.parser.core.models.SwaggerParseResult;
import java.util.Set;
import java.util.Objects;
import java.util.Optional;

class OpenAPIProcessor {

private final String operationId;

OpenAPIProcessor(String operationId) {
this.operationId = operationId;
this.operationId = Objects.requireNonNull(operationId);
}

public OperationDefinition parse(String content) {
OpenAPIParser parser = new OpenAPIParser();
ParseOptions opts = new ParseOptions();
opts.setResolve(true);
opts.setResolveFully(true);

SwaggerParseResult result = parser.readContents(content, null, opts);

if (result.getMessages() != null && !result.getMessages().isEmpty()) {
throw new IllegalArgumentException(
"Failed to parse OpenAPI document: " + String.join(", ", result.getMessages()));
}
return getOperation(result.getOpenAPI(), !result.isOpenapi31());
OperationDefinition parse(UnifiedOpenAPI unifiedOpenAPI) {
Optional<OperationDefinition> operationDefinition =
unifiedOpenAPI.findOperationById(this.operationId);
return operationDefinition.orElseThrow(
() ->
new IllegalArgumentException("Operation with ID '" + this.operationId + "' not found"));
}

private OperationDefinition getOperation(
OpenAPI openAPI, boolean emulateSwaggerV2BodyParameters) {
if (openAPI == null || openAPI.getPaths() == null) {
throw new IllegalArgumentException("Invalid OpenAPI document");
}

Set<String> paths = openAPI.getPaths().keySet();

for (String path : paths) {
PathItem pathItem = openAPI.getPaths().get(path);
OperationAndMethod operationAndMethod = findInPathItem(pathItem, operationId);
if (operationAndMethod != null) {
return new OperationDefinition(
openAPI,
operationAndMethod.operation,
path,
operationAndMethod.method,
emulateSwaggerV2BodyParameters);
}
}
throw new IllegalArgumentException(
"No operation with id '" + operationId + "' found in OpenAPI document");
}

private OperationAndMethod findInPathItem(PathItem pathItem, String operationId) {
if (pathItem == null) {
return null;
}

if (matches(pathItem.getGet(), operationId))
return new OperationAndMethod(pathItem.getGet(), "GET");
if (matches(pathItem.getPost(), operationId))
return new OperationAndMethod(pathItem.getPost(), "POST");
if (matches(pathItem.getPut(), operationId))
return new OperationAndMethod(pathItem.getPut(), "PUT");
if (matches(pathItem.getDelete(), operationId))
return new OperationAndMethod(pathItem.getDelete(), "DELETE");
if (matches(pathItem.getPatch(), operationId))
return new OperationAndMethod(pathItem.getPatch(), "PATCH");
if (matches(pathItem.getHead(), operationId))
return new OperationAndMethod(pathItem.getHead(), "HEAD");
if (matches(pathItem.getOptions(), operationId))
return new OperationAndMethod(pathItem.getOptions(), "OPTIONS");
if (matches(pathItem.getTrace(), operationId))
return new OperationAndMethod(pathItem.getTrace(), "TRACE");

return null;
}

private boolean matches(Operation op, String operationId) {
return op != null && operationId.equals(op.getOperationId());
}

private record OperationAndMethod(Operation operation, String method) {}
}
Loading