From 37a312dcd82820188783da30b2373a86e2ac4a1a Mon Sep 17 00:00:00 2001 From: Matheus Cruz Date: Tue, 23 Sep 2025 01:43:41 -0300 Subject: [PATCH 1/3] feat(openapi): support documentation example for openapi Signed-off-by: Matheus Cruz --- impl/openapi/pom.xml | 53 ++++++ .../impl/executors/OpenAPIExecutor.java | 152 ++++++++++++++++++ .../impl/executors/OpenAPIModelConverter.java | 32 ++++ .../executors/OpenAPIOperationContext.java | 59 +++++++ ...erlessworkflow.impl.executors.CallableTask | 1 + impl/pom.xml | 18 ++- impl/test/pom.xml | 4 + .../test/OpenAPIWorkflowDefinitionTest.java | 49 ++++++ .../openapi/findPetsByStatus.yaml | 14 ++ 9 files changed, 379 insertions(+), 3 deletions(-) create mode 100644 impl/openapi/pom.xml create mode 100644 impl/openapi/src/main/java/io/serverlessworkflow/impl/executors/OpenAPIExecutor.java create mode 100644 impl/openapi/src/main/java/io/serverlessworkflow/impl/executors/OpenAPIModelConverter.java create mode 100644 impl/openapi/src/main/java/io/serverlessworkflow/impl/executors/OpenAPIOperationContext.java create mode 100644 impl/openapi/src/main/resources/META-INF/services/io.serverlessworkflow.impl.executors.CallableTask create mode 100644 impl/test/src/test/java/io/serverlessworkflow/impl/test/OpenAPIWorkflowDefinitionTest.java create mode 100644 impl/test/src/test/resources/workflows-samples/openapi/findPetsByStatus.yaml diff --git a/impl/openapi/pom.xml b/impl/openapi/pom.xml new file mode 100644 index 00000000..dcda06e4 --- /dev/null +++ b/impl/openapi/pom.xml @@ -0,0 +1,53 @@ + + 4.0.0 + + io.serverlessworkflow + serverlessworkflow-impl + 8.0.0-SNAPSHOT + + serverlessworkflow-impl-openapi + Serverless Workflow :: Impl :: OpenAPI + + + org.glassfish.jersey.core + jersey-client + + + org.glassfish.jersey.media + jersey-media-json-jackson + + + io.serverlessworkflow + serverlessworkflow-impl-core + + + io.swagger.parser.v3 + swagger-parser + + + jakarta.ws.rs + jakarta.ws.rs-api + + + org.junit.jupiter + junit-jupiter-api + test + + + org.junit.jupiter + junit-jupiter-engine + test + + + org.junit.jupiter + junit-jupiter-params + test + + + org.assertj + assertj-core + test + + + \ No newline at end of file diff --git a/impl/openapi/src/main/java/io/serverlessworkflow/impl/executors/OpenAPIExecutor.java b/impl/openapi/src/main/java/io/serverlessworkflow/impl/executors/OpenAPIExecutor.java new file mode 100644 index 00000000..87e15a02 --- /dev/null +++ b/impl/openapi/src/main/java/io/serverlessworkflow/impl/executors/OpenAPIExecutor.java @@ -0,0 +1,152 @@ +/* + * 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; + +import io.serverlessworkflow.api.types.CallOpenAPI; +import io.serverlessworkflow.api.types.OpenAPIArguments; +import io.serverlessworkflow.api.types.TaskBase; +import io.serverlessworkflow.api.types.UriTemplate; +import io.serverlessworkflow.api.types.WithOpenAPIParameters; +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.WorkflowModel; +import io.serverlessworkflow.impl.resources.ResourceLoader; +import io.swagger.v3.oas.models.OpenAPI; +import io.swagger.v3.parser.OpenAPIV3Parser; +import jakarta.ws.rs.client.Client; +import jakarta.ws.rs.client.ClientBuilder; +import jakarta.ws.rs.client.Invocation; +import jakarta.ws.rs.client.WebTarget; +import jakarta.ws.rs.core.MultivaluedMap; +import java.net.URI; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.atomic.AtomicReference; + +public class OpenAPIExecutor implements CallableTask { + + private static final Client client = ClientBuilder.newClient(); + private WebTargetSupplier webTargetSupplier; + private RequestSupplier requestSupplier; + private final OpenAPIModelConverter converter = new OpenAPIModelConverter() {}; + + @FunctionalInterface + private interface WebTargetSupplier { + WebTarget apply(); + } + + @FunctionalInterface + private interface RequestSupplier { + WorkflowModel apply( + Invocation.Builder request, WorkflowContext workflow, TaskContext task, WorkflowModel node); + } + + @Override + public void init( + CallOpenAPI task, Workflow workflow, WorkflowApplication application, ResourceLoader loader) { + OpenAPIArguments args = task.getWith(); + WithOpenAPIParameters withParams = args.getParameters(); + + URI uri = getOpenAPIDocumentURI(args.getDocument().getEndpoint().getUriTemplate()); + + OpenAPIV3Parser apiv3Parser = new OpenAPIV3Parser(); + + OpenAPI openAPI = apiv3Parser.read(uri.toString()); + + OpenAPIOperationContext ctx = generateContext(openAPI, args, uri); + + this.webTargetSupplier = + () -> { + final AtomicReference webTarget = + new AtomicReference<>( + client + .target(openAPI.getServers().get(0).getUrl()) + .path(ctx.buildPath(withParams.getAdditionalProperties()))); + + MultivaluedMap queryParams = + ctx.buildQuery(withParams.getAdditionalProperties()); + queryParams.forEach( + (key, value) -> { + for (Object o : value) { + webTarget.set(webTarget.get().queryParam(key, o)); + } + }); + + return webTarget.get(); + }; + + this.requestSupplier = + (request, w, taskContext, node) -> { + Object response = request.method(ctx.httpMethodName(), node.objectClass()); + return converter.toModel(application.modelFactory(), node, response); + }; + } + + private static OpenAPIOperationContext generateContext( + OpenAPI openAPI, OpenAPIArguments args, URI uri) { + return openAPI.getPaths().entrySet().stream() + .flatMap( + pathEntry -> + pathEntry.getValue().readOperationsMap().entrySet().stream() + .map( + operationEntry -> + new OpenAPIOperationContext( + operationEntry.getValue().getOperationId(), + pathEntry.getKey(), + operationEntry.getKey(), + operationEntry.getValue()))) + .filter(c -> c.operationId().equals(args.getOperationId())) + .findFirst() + .orElseThrow( + () -> + new IllegalArgumentException( + "Operation with id " + + args.getOperationId() + + " not found in OpenAPI document " + + uri)); + } + + @Override + public CompletableFuture apply( + WorkflowContext workflowContext, TaskContext taskContext, WorkflowModel input) { + + return CompletableFuture.supplyAsync( + () -> { + WebTarget target = webTargetSupplier.apply(); + Invocation.Builder request = target.request(); + return requestSupplier.apply(request, workflowContext, taskContext, input); + }, + workflowContext.definition().application().executorService()); + } + + @Override + public boolean accept(Class clazz) { + return clazz.equals(CallOpenAPI.class); + } + + private static URI getOpenAPIDocumentURI(UriTemplate template) { + if (template.getLiteralUri() != null) { + return template.getLiteralUri(); + } else if (template.getLiteralUriTemplate() != null) { + // TODO: Support + // https://github.com/serverlessworkflow/specification/blob/main/dsl-reference.md#uri-template + throw new UnsupportedOperationException( + "URI templates with parameters are not supported yet"); + } + throw new IllegalArgumentException("Invalid UriTemplate definition " + template); + } +} diff --git a/impl/openapi/src/main/java/io/serverlessworkflow/impl/executors/OpenAPIModelConverter.java b/impl/openapi/src/main/java/io/serverlessworkflow/impl/executors/OpenAPIModelConverter.java new file mode 100644 index 00000000..c12fc811 --- /dev/null +++ b/impl/openapi/src/main/java/io/serverlessworkflow/impl/executors/OpenAPIModelConverter.java @@ -0,0 +1,32 @@ +/* + * 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; + +import io.serverlessworkflow.impl.WorkflowModel; +import io.serverlessworkflow.impl.WorkflowModelFactory; +import jakarta.ws.rs.client.Entity; +import java.util.Map; + +public interface OpenAPIModelConverter { + + default WorkflowModel toModel(WorkflowModelFactory factory, WorkflowModel model, Object entity) { + return factory.fromAny(model, entity); + } + + default Entity toEntity(Map model) { + return Entity.json(model); + } +} diff --git a/impl/openapi/src/main/java/io/serverlessworkflow/impl/executors/OpenAPIOperationContext.java b/impl/openapi/src/main/java/io/serverlessworkflow/impl/executors/OpenAPIOperationContext.java new file mode 100644 index 00000000..be190059 --- /dev/null +++ b/impl/openapi/src/main/java/io/serverlessworkflow/impl/executors/OpenAPIOperationContext.java @@ -0,0 +1,59 @@ +/* + * 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; + +import io.swagger.v3.oas.models.Operation; +import io.swagger.v3.oas.models.PathItem; +import io.swagger.v3.oas.models.parameters.Parameter; +import jakarta.ws.rs.core.MultivaluedHashMap; +import jakarta.ws.rs.core.MultivaluedMap; +import java.util.Map; + +public record OpenAPIOperationContext( + String operationId, String path, PathItem.HttpMethod httpMethod, Operation operation) { + + public String httpMethodName() { + return httpMethod.name(); + } + + public String buildPath(Map replacements) { + String finalPath = path; + for (Parameter parameter : operation.getParameters()) { + if ("path".equals(parameter.getIn())) { + String name = parameter.getName(); + Object value = replacements.get(name); + if (value != null) { + finalPath = path.replace("{" + name + "}", value.toString()); + } + } + } + return finalPath; + } + + public MultivaluedMap buildQuery(Map replacements) { + MultivaluedMap queryParams = new MultivaluedHashMap<>(); + for (Parameter parameter : operation.getParameters()) { + if ("query".equals(parameter.getIn())) { + String name = parameter.getName(); + Object value = replacements.get(name); + if (value != null) { + queryParams.add(name, value.toString()); + } + } + } + return queryParams; + } +} diff --git a/impl/openapi/src/main/resources/META-INF/services/io.serverlessworkflow.impl.executors.CallableTask b/impl/openapi/src/main/resources/META-INF/services/io.serverlessworkflow.impl.executors.CallableTask new file mode 100644 index 00000000..10d85299 --- /dev/null +++ b/impl/openapi/src/main/resources/META-INF/services/io.serverlessworkflow.impl.executors.CallableTask @@ -0,0 +1 @@ +io.serverlessworkflow.impl.executors.OpenAPIExecutor \ No newline at end of file diff --git a/impl/pom.xml b/impl/pom.xml index cd3f6076..2499840f 100644 --- a/impl/pom.xml +++ b/impl/pom.xml @@ -13,6 +13,7 @@ 1.6.0 5.2.3 4.0.0 + 2.1.34 @@ -36,6 +37,11 @@ serverlessworkflow-impl-jackson ${project.version} + + io.serverlessworkflow + serverlessworkflow-impl-openapi + ${project.version} + net.thisptr jackson-jq @@ -47,9 +53,9 @@ ${version.com.github.f4b6a3} - jakarta.ws.rs - jakarta.ws.rs-api - ${version.jakarta.ws.rs} + jakarta.ws.rs + jakarta.ws.rs-api + ${version.jakarta.ws.rs} org.glassfish.jersey.core @@ -63,6 +69,11 @@ ${version.org.glassfish.jersey} test + + io.swagger.parser.v3 + swagger-parser + ${version.io.swagger.parser.v3} + @@ -71,5 +82,6 @@ jackson jwt-impl test + openapi \ No newline at end of file diff --git a/impl/test/pom.xml b/impl/test/pom.xml index 1009da33..ff62abde 100644 --- a/impl/test/pom.xml +++ b/impl/test/pom.xml @@ -24,6 +24,10 @@ io.serverlessworkflow serverlessworkflow-impl-jackson-jwt + + io.serverlessworkflow + serverlessworkflow-impl-openapi + org.glassfish.jersey.media jersey-media-json-jackson diff --git a/impl/test/src/test/java/io/serverlessworkflow/impl/test/OpenAPIWorkflowDefinitionTest.java b/impl/test/src/test/java/io/serverlessworkflow/impl/test/OpenAPIWorkflowDefinitionTest.java new file mode 100644 index 00000000..29882e10 --- /dev/null +++ b/impl/test/src/test/java/io/serverlessworkflow/impl/test/OpenAPIWorkflowDefinitionTest.java @@ -0,0 +1,49 @@ +/* + * 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.test; + +import io.serverlessworkflow.api.WorkflowReader; +import io.serverlessworkflow.impl.WorkflowApplication; +import io.serverlessworkflow.impl.WorkflowModel; +import java.io.IOException; +import java.util.List; +import org.assertj.core.api.Assertions; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; + +public class OpenAPIWorkflowDefinitionTest { + + private static WorkflowApplication app; + + @BeforeAll + static void setUp() { + app = WorkflowApplication.builder().build(); + } + + @Test + void testOpenAPIWorkflowExecution() throws IOException { + + WorkflowModel model = + app.workflowDefinition( + WorkflowReader.readWorkflowFromClasspath( + "workflows-samples/openapi/findPetsByStatus.yaml")) + .instance(List.of()) + .start() + .join(); + + Assertions.assertThat(model.asCollection()).isNotEmpty(); + } +} diff --git a/impl/test/src/test/resources/workflows-samples/openapi/findPetsByStatus.yaml b/impl/test/src/test/resources/workflows-samples/openapi/findPetsByStatus.yaml new file mode 100644 index 00000000..eb76395f --- /dev/null +++ b/impl/test/src/test/resources/workflows-samples/openapi/findPetsByStatus.yaml @@ -0,0 +1,14 @@ +document: + dsl: '1.0.1' + namespace: test + name: openapi-example + version: '0.1.0' +do: + - findPet: + call: openapi + with: + document: + endpoint: https://petstore.swagger.io/v2/swagger.json + operationId: findPetsByStatus + parameters: + status: available \ No newline at end of file From ad8db5fa1ab116172c0393800c3a28bc27063638 Mon Sep 17 00:00:00 2001 From: Matheus Cruz Date: Tue, 23 Sep 2025 01:56:59 -0300 Subject: [PATCH 2/3] refactor(openapi): adjust code Signed-off-by: Matheus Cruz --- .../serverlessworkflow/impl/executors/OpenAPIExecutor.java | 2 +- .../impl/executors/OpenAPIOperationContext.java | 4 ++-- .../impl/test/OpenAPIWorkflowDefinitionTest.java | 7 +++++++ 3 files changed, 10 insertions(+), 3 deletions(-) diff --git a/impl/openapi/src/main/java/io/serverlessworkflow/impl/executors/OpenAPIExecutor.java b/impl/openapi/src/main/java/io/serverlessworkflow/impl/executors/OpenAPIExecutor.java index 87e15a02..08c9bf9b 100644 --- a/impl/openapi/src/main/java/io/serverlessworkflow/impl/executors/OpenAPIExecutor.java +++ b/impl/openapi/src/main/java/io/serverlessworkflow/impl/executors/OpenAPIExecutor.java @@ -78,7 +78,7 @@ public void init( .path(ctx.buildPath(withParams.getAdditionalProperties()))); MultivaluedMap queryParams = - ctx.buildQuery(withParams.getAdditionalProperties()); + ctx.buildQueryParams(withParams.getAdditionalProperties()); queryParams.forEach( (key, value) -> { for (Object o : value) { diff --git a/impl/openapi/src/main/java/io/serverlessworkflow/impl/executors/OpenAPIOperationContext.java b/impl/openapi/src/main/java/io/serverlessworkflow/impl/executors/OpenAPIOperationContext.java index be190059..fb2c8ebb 100644 --- a/impl/openapi/src/main/java/io/serverlessworkflow/impl/executors/OpenAPIOperationContext.java +++ b/impl/openapi/src/main/java/io/serverlessworkflow/impl/executors/OpenAPIOperationContext.java @@ -36,14 +36,14 @@ public String buildPath(Map replacements) { String name = parameter.getName(); Object value = replacements.get(name); if (value != null) { - finalPath = path.replace("{" + name + "}", value.toString()); + finalPath = path.replaceAll("\\{\\s*" + name + "\\s*}", value.toString()); } } } return finalPath; } - public MultivaluedMap buildQuery(Map replacements) { + public MultivaluedMap buildQueryParams(Map replacements) { MultivaluedMap queryParams = new MultivaluedHashMap<>(); for (Parameter parameter : operation.getParameters()) { if ("query".equals(parameter.getIn())) { diff --git a/impl/test/src/test/java/io/serverlessworkflow/impl/test/OpenAPIWorkflowDefinitionTest.java b/impl/test/src/test/java/io/serverlessworkflow/impl/test/OpenAPIWorkflowDefinitionTest.java index 29882e10..5d05012c 100644 --- a/impl/test/src/test/java/io/serverlessworkflow/impl/test/OpenAPIWorkflowDefinitionTest.java +++ b/impl/test/src/test/java/io/serverlessworkflow/impl/test/OpenAPIWorkflowDefinitionTest.java @@ -20,6 +20,7 @@ import io.serverlessworkflow.impl.WorkflowModel; import java.io.IOException; import java.util.List; +import java.util.Map; import org.assertj.core.api.Assertions; import org.junit.jupiter.api.BeforeAll; import org.junit.jupiter.api.Test; @@ -45,5 +46,11 @@ void testOpenAPIWorkflowExecution() throws IOException { .join(); Assertions.assertThat(model.asCollection()).isNotEmpty(); + Assertions.assertThat(model.asCollection()) + .allMatch( + m -> { + Map pet = m.asMap().orElseThrow(RuntimeException::new); + return pet.get("status").equals("available"); + }); } } From f671a1c7bb29bd4a99c6b7ed6af0c184d1fa83a3 Mon Sep 17 00:00:00 2001 From: Matheus Cruz Date: Tue, 23 Sep 2025 12:11:23 -0300 Subject: [PATCH 3/3] feat(openapi): add redirect rules Signed-off-by: Matheus Cruz --- .../impl/executors/OpenAPIExecutor.java | 96 ++++++++++++++----- .../executors/OpenAPIOperationContext.java | 7 ++ .../test/OpenAPIWorkflowDefinitionTest.java | 71 +++++++++++++- .../openapi/findPetsByStatus-redirect.yaml | 13 +++ 4 files changed, 161 insertions(+), 26 deletions(-) create mode 100644 impl/test/src/test/resources/workflows-samples/openapi/findPetsByStatus-redirect.yaml diff --git a/impl/openapi/src/main/java/io/serverlessworkflow/impl/executors/OpenAPIExecutor.java b/impl/openapi/src/main/java/io/serverlessworkflow/impl/executors/OpenAPIExecutor.java index 08c9bf9b..ee27be90 100644 --- a/impl/openapi/src/main/java/io/serverlessworkflow/impl/executors/OpenAPIExecutor.java +++ b/impl/openapi/src/main/java/io/serverlessworkflow/impl/executors/OpenAPIExecutor.java @@ -24,25 +24,31 @@ import io.serverlessworkflow.impl.TaskContext; import io.serverlessworkflow.impl.WorkflowApplication; import io.serverlessworkflow.impl.WorkflowContext; +import io.serverlessworkflow.impl.WorkflowError; +import io.serverlessworkflow.impl.WorkflowException; import io.serverlessworkflow.impl.WorkflowModel; import io.serverlessworkflow.impl.resources.ResourceLoader; import io.swagger.v3.oas.models.OpenAPI; import io.swagger.v3.parser.OpenAPIV3Parser; +import jakarta.ws.rs.WebApplicationException; import jakarta.ws.rs.client.Client; import jakarta.ws.rs.client.ClientBuilder; import jakarta.ws.rs.client.Invocation; import jakarta.ws.rs.client.WebTarget; import jakarta.ws.rs.core.MultivaluedMap; +import jakarta.ws.rs.core.Response; import java.net.URI; +import java.util.List; +import java.util.Map; +import java.util.Optional; import java.util.concurrent.CompletableFuture; -import java.util.concurrent.atomic.AtomicReference; public class OpenAPIExecutor implements CallableTask { private static final Client client = ClientBuilder.newClient(); private WebTargetSupplier webTargetSupplier; private RequestSupplier requestSupplier; - private final OpenAPIModelConverter converter = new OpenAPIModelConverter() {}; + private OpenAPIModelConverter converter = new OpenAPIModelConverter() {}; @FunctionalInterface private interface WebTargetSupplier { @@ -59,7 +65,6 @@ WorkflowModel apply( public void init( CallOpenAPI task, Workflow workflow, WorkflowApplication application, ResourceLoader loader) { OpenAPIArguments args = task.getWith(); - WithOpenAPIParameters withParams = args.getParameters(); URI uri = getOpenAPIDocumentURI(args.getDocument().getEndpoint().getUriTemplate()); @@ -69,33 +74,75 @@ public void init( OpenAPIOperationContext ctx = generateContext(openAPI, args, uri); - this.webTargetSupplier = - () -> { - final AtomicReference webTarget = - new AtomicReference<>( - client - .target(openAPI.getServers().get(0).getUrl()) - .path(ctx.buildPath(withParams.getAdditionalProperties()))); - - MultivaluedMap queryParams = - ctx.buildQueryParams(withParams.getAdditionalProperties()); - queryParams.forEach( - (key, value) -> { - for (Object o : value) { - webTarget.set(webTarget.get().queryParam(key, o)); - } - }); - - return webTarget.get(); - }; + WithOpenAPIParameters withParams = + Optional.ofNullable(args.getParameters()).orElse(new WithOpenAPIParameters()); + + this.webTargetSupplier = getTargetSupplier(openAPI, ctx, withParams); this.requestSupplier = (request, w, taskContext, node) -> { - Object response = request.method(ctx.httpMethodName(), node.objectClass()); - return converter.toModel(application.modelFactory(), node, response); + try { + Response response = request.method(ctx.httpMethodName(), Response.class); + + if (!args.isRedirect() && !is2xx(response)) { + throw new WorkflowException( + WorkflowError.communication( + response.getStatus(), + taskContext, + "Received a non-2xx nor 3xx response but redirects are enabled") + .build()); + } + + if (args.isRedirect() && isNot2xxNor3xx(response)) { + throw new WorkflowException( + WorkflowError.communication( + response.getStatus(), + taskContext, + "Received a non-2xx nor 3xx response but redirects are enabled") + .build()); + } + + return converter.toModel( + application.modelFactory(), node, response.readEntity(node.objectClass())); + } catch (WebApplicationException exception) { + throw new WorkflowException( + WorkflowError.communication( + exception.getResponse().getStatus(), taskContext, exception) + .build()); + } }; } + private static WebTargetSupplier getTargetSupplier( + OpenAPI openAPI, OpenAPIOperationContext ctx, WithOpenAPIParameters withParams) { + return () -> { + WebTarget webTarget = + client + .target(openAPI.getServers().get(0).getUrl()) + .path(ctx.buildPath(withParams.getAdditionalProperties())); + + MultivaluedMap queryParams = + ctx.buildQueryParams(withParams.getAdditionalProperties()); + + for (Map.Entry> queryParam : queryParams.entrySet()) { + for (Object value : queryParam.getValue()) { + webTarget = webTarget.queryParam(queryParam.getKey(), value); + } + } + + return webTarget; + }; + } + + private static boolean is2xx(Response response) { + return response.getStatusInfo().getFamily().equals(Response.Status.Family.SUCCESSFUL); + } + + private static boolean isNot2xxNor3xx(Response response) { + return !(response.getStatusInfo().getFamily().equals(Response.Status.Family.SUCCESSFUL) + || response.getStatusInfo().getFamily().equals(Response.Status.Family.REDIRECTION)); + } + private static OpenAPIOperationContext generateContext( OpenAPI openAPI, OpenAPIArguments args, URI uri) { return openAPI.getPaths().entrySet().stream() @@ -142,7 +189,6 @@ private static URI getOpenAPIDocumentURI(UriTemplate template) { if (template.getLiteralUri() != null) { return template.getLiteralUri(); } else if (template.getLiteralUriTemplate() != null) { - // TODO: Support // https://github.com/serverlessworkflow/specification/blob/main/dsl-reference.md#uri-template throw new UnsupportedOperationException( "URI templates with parameters are not supported yet"); diff --git a/impl/openapi/src/main/java/io/serverlessworkflow/impl/executors/OpenAPIOperationContext.java b/impl/openapi/src/main/java/io/serverlessworkflow/impl/executors/OpenAPIOperationContext.java index fb2c8ebb..eb89e326 100644 --- a/impl/openapi/src/main/java/io/serverlessworkflow/impl/executors/OpenAPIOperationContext.java +++ b/impl/openapi/src/main/java/io/serverlessworkflow/impl/executors/OpenAPIOperationContext.java @@ -21,6 +21,7 @@ import jakarta.ws.rs.core.MultivaluedHashMap; import jakarta.ws.rs.core.MultivaluedMap; import java.util.Map; +import java.util.Objects; public record OpenAPIOperationContext( String operationId, String path, PathItem.HttpMethod httpMethod, Operation operation) { @@ -31,6 +32,9 @@ public String httpMethodName() { public String buildPath(Map replacements) { String finalPath = path; + if (Objects.isNull(operation.getParameters())) { + return ""; + } for (Parameter parameter : operation.getParameters()) { if ("path".equals(parameter.getIn())) { String name = parameter.getName(); @@ -44,6 +48,9 @@ public String buildPath(Map replacements) { } public MultivaluedMap buildQueryParams(Map replacements) { + if (Objects.isNull(operation.getParameters())) { + return new MultivaluedHashMap<>(); + } MultivaluedMap queryParams = new MultivaluedHashMap<>(); for (Parameter parameter : operation.getParameters()) { if ("query".equals(parameter.getIn())) { diff --git a/impl/test/src/test/java/io/serverlessworkflow/impl/test/OpenAPIWorkflowDefinitionTest.java b/impl/test/src/test/java/io/serverlessworkflow/impl/test/OpenAPIWorkflowDefinitionTest.java index 5d05012c..d800d8cd 100644 --- a/impl/test/src/test/java/io/serverlessworkflow/impl/test/OpenAPIWorkflowDefinitionTest.java +++ b/impl/test/src/test/java/io/serverlessworkflow/impl/test/OpenAPIWorkflowDefinitionTest.java @@ -17,20 +17,38 @@ import io.serverlessworkflow.api.WorkflowReader; import io.serverlessworkflow.impl.WorkflowApplication; +import io.serverlessworkflow.impl.WorkflowException; import io.serverlessworkflow.impl.WorkflowModel; import java.io.IOException; import java.util.List; import java.util.Map; +import okhttp3.mockwebserver.MockResponse; +import okhttp3.mockwebserver.MockWebServer; import org.assertj.core.api.Assertions; +import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; public class OpenAPIWorkflowDefinitionTest { private static WorkflowApplication app; + private MockWebServer mockServer; + + @BeforeEach + public void setUp() throws IOException { + mockServer = new MockWebServer(); + mockServer.start(9999); + } + + @AfterEach + void tearDown() throws IOException { + mockServer.shutdown(); + } @BeforeAll - static void setUp() { + static void setUpApp() { app = WorkflowApplication.builder().build(); } @@ -53,4 +71,55 @@ void testOpenAPIWorkflowExecution() throws IOException { return pet.get("status").equals("available"); }); } + + @Test + @DisplayName( + "must raise an error for response status codes outside the 200–299 range when redirect is set to false") + void testOpenAPIRedirect() { + mockServer.enqueue( + new MockResponse() + .setResponseCode(200) + .setBody( + """ + { + "openapi": "3.0.3", + "info": { + "title": "Redirect API", + "version": "1.0.0" + }, + "servers": [ + { + "url": "http://localhost:9999" + } + ], + "paths": { + "/redirect": { + "get": { + "operationId": "redirectToDocs", + "summary": "Redirects to external documentation", + "responses": { + "302": { + "description": "Redirecting to external documentation" + } + } + } + } + } + } + } + """) + .setHeader("Content-Type", "application/json")); + + mockServer.enqueue(new MockResponse().setResponseCode(301)); + + Assertions.assertThatThrownBy( + () -> + app.workflowDefinition( + WorkflowReader.readWorkflowFromClasspath( + "workflows-samples/openapi/findPetsByStatus-redirect.yaml")) + .instance(List.of()) + .start() + .join()) + .hasCauseInstanceOf(WorkflowException.class); + } } diff --git a/impl/test/src/test/resources/workflows-samples/openapi/findPetsByStatus-redirect.yaml b/impl/test/src/test/resources/workflows-samples/openapi/findPetsByStatus-redirect.yaml new file mode 100644 index 00000000..f99d5f9b --- /dev/null +++ b/impl/test/src/test/resources/workflows-samples/openapi/findPetsByStatus-redirect.yaml @@ -0,0 +1,13 @@ +document: + dsl: '1.0.1' + namespace: test + name: openapi-example-with-redirect + version: '0.1.0' +do: + - findPet: + call: openapi + with: + document: + endpoint: http://localhost:9999 + operationId: redirectToDocs + redirect: false \ No newline at end of file