diff --git a/impl/openapi/pom.xml b/impl/openapi/pom.xml
new file mode 100644
index 00000000..db6e43c0
--- /dev/null
+++ b/impl/openapi/pom.xml
@@ -0,0 +1,30 @@
+
+ 4.0.0
+
+ io.serverlessworkflow
+ serverlessworkflow-impl
+ 8.0.0-SNAPSHOT
+
+ serverlessworkflow-impl-openapi
+ Serverless Workflow :: Impl :: OpenAPI
+
+
+ jakarta.ws.rs
+ jakarta.ws.rs-api
+
+
+ io.serverlessworkflow
+ serverlessworkflow-impl-core
+
+
+ io.serverlessworkflow
+ serverlessworkflow-impl-http
+
+
+ io.swagger.parser.v3
+ swagger-parser
+ ${version.io.swagger.parser.v3}
+
+
+
diff --git a/impl/openapi/src/main/java/io/serverlessworkflow/impl/executors/openapi/ExpressionURISupplier.java b/impl/openapi/src/main/java/io/serverlessworkflow/impl/executors/openapi/ExpressionURISupplier.java
new file mode 100644
index 00000000..e5e256ca
--- /dev/null
+++ b/impl/openapi/src/main/java/io/serverlessworkflow/impl/executors/openapi/ExpressionURISupplier.java
@@ -0,0 +1,35 @@
+/*
+ * 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 io.serverlessworkflow.impl.TaskContext;
+import io.serverlessworkflow.impl.WorkflowContext;
+import io.serverlessworkflow.impl.WorkflowModel;
+import io.serverlessworkflow.impl.WorkflowValueResolver;
+import java.net.URI;
+
+class ExpressionURISupplier implements TargetSupplier {
+ private WorkflowValueResolver resolver;
+
+ ExpressionURISupplier(WorkflowValueResolver resolver) {
+ this.resolver = resolver;
+ }
+
+ @Override
+ public URI apply(WorkflowContext workflow, TaskContext task, WorkflowModel node) {
+ return URI.create(resolver.apply(workflow, task, node));
+ }
+}
diff --git a/impl/openapi/src/main/java/io/serverlessworkflow/impl/executors/openapi/HttpCallAdapter.java b/impl/openapi/src/main/java/io/serverlessworkflow/impl/executors/openapi/HttpCallAdapter.java
new file mode 100644
index 00000000..11607f9c
--- /dev/null
+++ b/impl/openapi/src/main/java/io/serverlessworkflow/impl/executors/openapi/HttpCallAdapter.java
@@ -0,0 +1,211 @@
+/*
+ * 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 io.serverlessworkflow.api.types.CallHTTP;
+import io.serverlessworkflow.api.types.Endpoint;
+import io.serverlessworkflow.api.types.EndpointConfiguration;
+import io.serverlessworkflow.api.types.HTTPArguments;
+import io.serverlessworkflow.api.types.HTTPHeaders;
+import io.serverlessworkflow.api.types.HTTPQuery;
+import io.serverlessworkflow.api.types.Headers;
+import io.serverlessworkflow.api.types.Query;
+import io.serverlessworkflow.api.types.ReferenceableAuthenticationPolicy;
+import io.serverlessworkflow.api.types.TaskTimeout;
+import io.serverlessworkflow.api.types.Timeout;
+import io.serverlessworkflow.api.types.TimeoutAfter;
+import io.serverlessworkflow.api.types.UriTemplate;
+import io.swagger.v3.oas.models.media.Schema;
+import io.swagger.v3.oas.models.parameters.Parameter;
+import java.net.URI;
+import java.util.Collection;
+import java.util.LinkedHashMap;
+import java.util.Map;
+
+@SuppressWarnings("rawtypes")
+class HttpCallAdapter {
+
+ private ReferenceableAuthenticationPolicy auth;
+ private Map body;
+ private String contentType;
+ private Collection headers;
+ private String method;
+ private Collection query;
+ private boolean redirect;
+ private URI server;
+ private URI target;
+ private Map workflowParams;
+
+ HttpCallAdapter auth(ReferenceableAuthenticationPolicy policy) {
+ if (policy != null) {
+ this.auth = policy;
+ }
+ return this;
+ }
+
+ HttpCallAdapter body(Map body) {
+ this.body = body;
+ return this;
+ }
+
+ CallHTTP build() {
+ CallHTTP callHTTP = new CallHTTP();
+
+ HTTPArguments httpArgs = new HTTPArguments();
+ callHTTP.withWith(httpArgs);
+
+ Endpoint endpoint = new Endpoint();
+ httpArgs.withEndpoint(endpoint);
+
+ if (this.auth != null) {
+ EndpointConfiguration endPointConfig = new EndpointConfiguration();
+ endPointConfig.setAuthentication(this.auth);
+ endpoint.setEndpointConfiguration(endPointConfig);
+ }
+
+ httpArgs.setRedirect(this.redirect);
+ httpArgs.setMethod(this.method);
+
+ addHttpHeaders(httpArgs);
+ addQueryParams(httpArgs);
+ addBody(httpArgs);
+
+ addTarget(endpoint);
+
+ TaskTimeout taskTimeout = new TaskTimeout();
+ Timeout timeout = new Timeout();
+ taskTimeout.withTaskTimeoutDefinition(timeout);
+ TimeoutAfter timeoutAfter = new TimeoutAfter();
+ timeout.setAfter(timeoutAfter);
+ timeoutAfter.withDurationExpression("PT30S");
+ callHTTP.setTimeout(taskTimeout);
+
+ return callHTTP;
+ }
+
+ HttpCallAdapter contentType(String contentType) {
+ this.contentType = contentType;
+ return this;
+ }
+
+ HttpCallAdapter headers(Collection headers) {
+ this.headers = headers;
+ return this;
+ }
+
+ HttpCallAdapter method(String method) {
+ this.method = method;
+ return this;
+ }
+
+ HttpCallAdapter query(Collection query) {
+ this.query = query;
+ return this;
+ }
+
+ HttpCallAdapter redirect(boolean redirect) {
+ this.redirect = redirect;
+ return this;
+ }
+
+ HttpCallAdapter server(String server) {
+ this.server = URI.create(server);
+ return this;
+ }
+
+ HttpCallAdapter target(URI target) {
+ this.target = target;
+ return this;
+ }
+
+ HttpCallAdapter workflowParams(Map workflowParams) {
+ this.workflowParams = workflowParams;
+ return this;
+ }
+
+ private void addBody(HTTPArguments httpArgs) {
+ Map bodyContent = new LinkedHashMap<>();
+ if (!(body == null || body.isEmpty())) {
+ for (Map.Entry entry : body.entrySet()) {
+ String name = entry.getKey();
+ if (workflowParams.containsKey(name)) {
+ Object value = workflowParams.get(name);
+ bodyContent.put(name, value);
+ }
+ }
+ if (!bodyContent.isEmpty()) {
+ httpArgs.setBody(bodyContent);
+ }
+ }
+ }
+
+ private void addHttpHeaders(HTTPArguments httpArgs) {
+ if (!(headers == null || headers.isEmpty())) {
+ Headers hdrs = new Headers();
+ HTTPHeaders httpHeaders = new HTTPHeaders();
+ hdrs.setHTTPHeaders(httpHeaders);
+ httpArgs.setHeaders(hdrs);
+
+ for (Parameter p : headers) {
+ String name = p.getName();
+ if (workflowParams.containsKey(name)) {
+ Object value = workflowParams.get(name);
+ if (value instanceof String asString) {
+ httpHeaders.setAdditionalProperty(name, asString);
+ } else {
+ throw new IllegalArgumentException("Header parameter " + name + " must be a String");
+ }
+ }
+ }
+ }
+ }
+
+ private void addQueryParams(HTTPArguments httpArgs) {
+ if (!(query == null || query.isEmpty())) {
+ Query queryParams = new Query();
+ httpArgs.setQuery(queryParams);
+ HTTPQuery httpQuery = new HTTPQuery();
+ queryParams.setHTTPQuery(httpQuery);
+
+ for (Parameter p : query) {
+ String name = p.getName();
+ if (workflowParams.containsKey(name)) {
+ Object value = workflowParams.get(name);
+ if (value instanceof String asString) {
+ httpQuery.setAdditionalProperty(name, asString);
+ } else if (value instanceof Number asNumber) {
+ httpQuery.setAdditionalProperty(name, asNumber.toString());
+ } else if (value instanceof Boolean asBoolean) {
+ httpQuery.setAdditionalProperty(name, asBoolean.toString());
+ } else if (value instanceof Character asCharacter) {
+ httpQuery.setAdditionalProperty(name, asCharacter.toString());
+ } else {
+ httpQuery.setAdditionalProperty(name, value.toString());
+ }
+ }
+ }
+ }
+ }
+
+ private void addTarget(Endpoint endpoint) {
+ if (this.target == null) {
+ throw new IllegalArgumentException("No Server defined for the OpenAPI operation");
+ }
+ UriTemplate uriTemplate = new UriTemplate();
+ uriTemplate.withLiteralUri(this.server.resolve(this.target.getPath()));
+ endpoint.setUriTemplate(uriTemplate);
+ }
+}
diff --git a/impl/openapi/src/main/java/io/serverlessworkflow/impl/executors/openapi/OpenAPIExecutor.java b/impl/openapi/src/main/java/io/serverlessworkflow/impl/executors/openapi/OpenAPIExecutor.java
new file mode 100644
index 00000000..95fa1fd0
--- /dev/null
+++ b/impl/openapi/src/main/java/io/serverlessworkflow/impl/executors/openapi/OpenAPIExecutor.java
@@ -0,0 +1,154 @@
+/*
+ * 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 io.serverlessworkflow.api.types.CallHTTP;
+import io.serverlessworkflow.api.types.CallOpenAPI;
+import io.serverlessworkflow.api.types.Endpoint;
+import io.serverlessworkflow.api.types.EndpointUri;
+import io.serverlessworkflow.api.types.TaskBase;
+import io.serverlessworkflow.api.types.UriTemplate;
+import io.serverlessworkflow.api.types.Workflow;
+import io.serverlessworkflow.impl.TaskContext;
+import io.serverlessworkflow.impl.WorkflowApplication;
+import io.serverlessworkflow.impl.WorkflowContext;
+import io.serverlessworkflow.impl.WorkflowException;
+import io.serverlessworkflow.impl.WorkflowModel;
+import io.serverlessworkflow.impl.executors.CallableTask;
+import io.serverlessworkflow.impl.executors.http.HttpExecutor;
+import io.serverlessworkflow.impl.expressions.ExpressionDescriptor;
+import io.serverlessworkflow.impl.expressions.ExpressionFactory;
+import io.serverlessworkflow.impl.resources.ResourceLoader;
+import jakarta.ws.rs.core.UriBuilder;
+import java.net.URI;
+import java.util.concurrent.CompletableFuture;
+import java.util.stream.Collectors;
+
+public class OpenAPIExecutor implements CallableTask {
+
+ private CallOpenAPI task;
+ private Workflow workflow;
+ private WorkflowApplication application;
+ private TargetSupplier targetSupplier;
+
+ private ResourceLoader resourceLoader;
+
+ @Override
+ public boolean accept(Class extends TaskBase> clazz) {
+ return clazz.equals(CallOpenAPI.class);
+ }
+
+ @Override
+ public CompletableFuture apply(
+ WorkflowContext workflowContext, TaskContext taskContext, WorkflowModel input) {
+ String operationId = task.getWith().getOperationId();
+ URI openAPIEndpoint = targetSupplier.apply(workflowContext, taskContext, input);
+ OpenAPIProcessor processor = new OpenAPIProcessor(operationId, openAPIEndpoint);
+ OperationDefinition operation = processor.parse();
+
+ OperationPathResolver pathResolver =
+ new OperationPathResolver(
+ operation.getPath(),
+ application,
+ task.getWith().getParameters().getAdditionalProperties());
+
+ return CompletableFuture.supplyAsync(
+ () -> {
+ HttpCallAdapter httpCallAdapter =
+ new HttpCallAdapter()
+ .auth(task.getWith().getAuthentication())
+ .body(operation.getBody())
+ .contentType(operation.getContentType())
+ .headers(
+ operation.getParameters().stream()
+ .filter(p -> "header".equals(p.getIn()))
+ .collect(Collectors.toUnmodifiableSet()))
+ .method(operation.getMethod())
+ .query(
+ operation.getParameters().stream()
+ .filter(p -> "query".equals(p.getIn()))
+ .collect(Collectors.toUnmodifiableSet()))
+ .redirect(task.getWith().isRedirect())
+ .target(pathResolver.resolve(workflowContext, taskContext, input))
+ .workflowParams(task.getWith().getParameters().getAdditionalProperties());
+
+ WorkflowException workflowException = null;
+
+ for (var server : operation.getServers()) {
+ CallHTTP callHTTP = httpCallAdapter.server(server).build();
+ HttpExecutor executor = new HttpExecutor();
+ executor.init(callHTTP, workflow, application, resourceLoader);
+
+ try {
+ return executor.apply(workflowContext, taskContext, input).get();
+ } catch (WorkflowException e) {
+ workflowException = e;
+ } catch (Exception e) {
+ throw new RuntimeException(e);
+ }
+ }
+ throw workflowException; // if we there, we failed all servers and ex is not null
+ },
+ workflowContext.definition().application().executorService());
+ }
+
+ @Override
+ public void init(
+ CallOpenAPI task,
+ Workflow workflow,
+ WorkflowApplication application,
+ ResourceLoader resourceLoader) {
+ this.task = task;
+ this.workflow = workflow;
+ this.application = application;
+ this.targetSupplier =
+ getTargetSupplier(
+ task.getWith().getDocument().getEndpoint(), application.expressionFactory());
+ this.resourceLoader = resourceLoader;
+ }
+
+ private TargetSupplier getTargetSupplier(Endpoint endpoint, ExpressionFactory expressionFactory) {
+ if (endpoint.getEndpointConfiguration() != null) {
+ EndpointUri uri = endpoint.getEndpointConfiguration().getUri();
+ if (uri.getLiteralEndpointURI() != null) {
+ return getURISupplier(uri.getLiteralEndpointURI());
+ } else if (uri.getExpressionEndpointURI() != null) {
+ return new ExpressionURISupplier(
+ expressionFactory.resolveString(
+ ExpressionDescriptor.from(uri.getExpressionEndpointURI())));
+ }
+ } else if (endpoint.getRuntimeExpression() != null) {
+ return new ExpressionURISupplier(
+ expressionFactory.resolveString(
+ ExpressionDescriptor.from(endpoint.getRuntimeExpression())));
+ } else if (endpoint.getUriTemplate() != null) {
+ return getURISupplier(endpoint.getUriTemplate());
+ }
+ throw new IllegalArgumentException("Invalid endpoint definition " + endpoint);
+ }
+
+ private TargetSupplier getURISupplier(UriTemplate template) {
+ if (template.getLiteralUri() != null) {
+ return (w, t, n) -> template.getLiteralUri();
+ } else if (template.getLiteralUriTemplate() != null) {
+ return (w, t, n) ->
+ UriBuilder.fromUri(template.getLiteralUriTemplate())
+ .resolveTemplates(n.asMap().orElseThrow(), false)
+ .build();
+ }
+ throw new IllegalArgumentException("Invalid uri template definition " + template);
+ }
+}
diff --git a/impl/openapi/src/main/java/io/serverlessworkflow/impl/executors/openapi/OpenAPIProcessor.java b/impl/openapi/src/main/java/io/serverlessworkflow/impl/executors/openapi/OpenAPIProcessor.java
new file mode 100644
index 00000000..2e05e5fe
--- /dev/null
+++ b/impl/openapi/src/main/java/io/serverlessworkflow/impl/executors/openapi/OpenAPIProcessor.java
@@ -0,0 +1,97 @@
+/*
+ * 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 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.OpenAPIV3Parser;
+import io.swagger.v3.parser.core.models.ParseOptions;
+import java.net.URI;
+import java.util.List;
+import java.util.Set;
+
+class OpenAPIProcessor {
+
+ private final String operationId;
+ private final URI openAPIEndpoint;
+
+ OpenAPIProcessor(String operationId, URI openAPIEndpoint) {
+ this.operationId = operationId;
+ this.openAPIEndpoint = openAPIEndpoint;
+ }
+
+ OperationDefinition parse() {
+ OpenAPIV3Parser parser = new OpenAPIV3Parser();
+ ParseOptions opts = new ParseOptions();
+ opts.setResolve(true);
+ opts.setResolveFully(false);
+
+ var result = parser.readLocation(openAPIEndpoint.toString(), List.of(), opts);
+ var openapi = result.getOpenAPI();
+ return getOperation(openapi);
+ }
+
+ OperationDefinition getOperation(OpenAPI openAPI) {
+ if (openAPI == null || openAPI.getPaths() == null) {
+ throw new IllegalArgumentException("Invalid OpenAPI document");
+ }
+
+ Set 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);
+ }
+ }
+ 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) {}
+}
diff --git a/impl/openapi/src/main/java/io/serverlessworkflow/impl/executors/openapi/OperationDefinition.java b/impl/openapi/src/main/java/io/serverlessworkflow/impl/executors/openapi/OperationDefinition.java
new file mode 100644
index 00000000..0ffc36b4
--- /dev/null
+++ b/impl/openapi/src/main/java/io/serverlessworkflow/impl/executors/openapi/OperationDefinition.java
@@ -0,0 +1,132 @@
+/*
+ * 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 io.swagger.v3.oas.models.OpenAPI;
+import io.swagger.v3.oas.models.Operation;
+import io.swagger.v3.oas.models.media.Content;
+import io.swagger.v3.oas.models.media.MediaType;
+import io.swagger.v3.oas.models.media.Schema;
+import io.swagger.v3.oas.models.parameters.Parameter;
+import io.swagger.v3.oas.models.responses.ApiResponse;
+import io.swagger.v3.oas.models.servers.Server;
+import java.util.List;
+import java.util.Map;
+
+class OperationDefinition {
+ private final Operation operation;
+ private final String method;
+ private final OpenAPI openAPI;
+ private final String path;
+
+ OperationDefinition(OpenAPI openAPI, Operation operation, String path, String method) {
+ this.openAPI = openAPI;
+ this.operation = operation;
+ this.path = path;
+ this.method = method;
+ }
+
+ String getMethod() {
+ return method;
+ }
+
+ String getPath() {
+ return path;
+ }
+
+ Operation getOperation() {
+ return operation;
+ }
+
+ List getServers() {
+ return openAPI.getServers().stream().map(Server::getUrl).toList();
+ }
+
+ List getParameters() {
+ if (operation.getParameters() == null) {
+ return List.of();
+ }
+ return operation.getParameters();
+ }
+
+ @SuppressWarnings({"rawtypes", "unchecked"})
+ Map getBody() {
+ if (operation.getRequestBody() != null && operation.getRequestBody().getContent() != null) {
+ Content content = operation.getRequestBody().getContent();
+ if (content.containsKey("application/json")) {
+ MediaType mt = content.get("application/json");
+ if (mt.getSchema().get$ref() != null && !mt.getSchema().get$ref().isEmpty()) {
+ Schema> schema = resolveSchema(mt.getSchema().get$ref());
+ return schema.getProperties();
+ } else if (mt.getSchema().getProperties() != null) {
+ return mt.getSchema().getProperties();
+ } else {
+ throw new IllegalArgumentException(
+ "Can't resolve schema for request body of operation " + operation.getOperationId());
+ }
+ } else {
+ throw new IllegalArgumentException("Only 'application/json' content type is supported");
+ }
+ }
+ return Map.of();
+ }
+
+ String getContentType() {
+ String method = getMethod().toUpperCase();
+
+ if (method.equals("POST") || method.equals("PUT") || method.equals("PATCH")) {
+ if (operation.getRequestBody() != null && operation.getRequestBody().getContent() != null) {
+ Content content = operation.getRequestBody().getContent();
+ if (!content.isEmpty()) {
+ return content.keySet().iterator().next();
+ }
+ }
+ }
+
+ if (operation.getResponses() != null) {
+ for (String code : new String[] {"200", "201", "204"}) {
+ ApiResponse resp = operation.getResponses().get(code);
+ if (resp != null && resp.getContent() != null && !resp.getContent().isEmpty()) {
+ return resp.getContent().keySet().iterator().next();
+ }
+ }
+ for (Map.Entry e : operation.getResponses().entrySet()) {
+ Content content = e.getValue().getContent();
+ if (content != null && !content.isEmpty()) {
+ return content.keySet().iterator().next();
+ }
+ }
+ }
+
+ throw new IllegalStateException(
+ "No content type found for operation " + operation.getOperationId() + " [" + method + "]");
+ }
+
+ Schema> resolveSchema(String ref) {
+ if (ref == null || !ref.startsWith("#/components/schemas/")) {
+ throw new IllegalArgumentException("Unsupported $ref format: " + ref);
+ }
+ String name = ref.substring("#/components/schemas/".length());
+ if (openAPI.getComponents() == null || openAPI.getComponents().getSchemas() == null) {
+ throw new IllegalStateException("No components/schemas found in OpenAPI");
+ }
+ Schema> schema = openAPI.getComponents().getSchemas().get(name);
+ if (schema == null) {
+ throw new IllegalArgumentException("Schema not found: " + name);
+ }
+ return schema;
+ }
+}
diff --git a/impl/openapi/src/main/java/io/serverlessworkflow/impl/executors/openapi/OperationPathResolver.java b/impl/openapi/src/main/java/io/serverlessworkflow/impl/executors/openapi/OperationPathResolver.java
new file mode 100644
index 00000000..d9c808e4
--- /dev/null
+++ b/impl/openapi/src/main/java/io/serverlessworkflow/impl/executors/openapi/OperationPathResolver.java
@@ -0,0 +1,47 @@
+/*
+ * 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 io.serverlessworkflow.impl.TaskContext;
+import io.serverlessworkflow.impl.WorkflowApplication;
+import io.serverlessworkflow.impl.WorkflowContext;
+import io.serverlessworkflow.impl.WorkflowModel;
+import io.serverlessworkflow.impl.WorkflowValueResolver;
+import io.serverlessworkflow.impl.expressions.ExpressionDescriptor;
+import jakarta.ws.rs.core.UriBuilder;
+import java.net.URI;
+import java.util.Map;
+
+class OperationPathResolver {
+
+ private final Map args;
+ private final String path;
+ private WorkflowApplication application;
+
+ OperationPathResolver(String path, WorkflowApplication application, Map args) {
+ this.path = path;
+ this.args = args;
+ this.application = application;
+ }
+
+ URI resolve(WorkflowContext workflow, TaskContext task, WorkflowModel model) {
+ WorkflowValueResolver