From cd046db0452f88b494e6497b0de676765ce08949 Mon Sep 17 00:00:00 2001 From: Matheus Cruz Date: Wed, 15 Apr 2026 12:00:48 -0300 Subject: [PATCH 1/3] Add automatic URI detection for OpenAPI document configuration - Enhanced CallOpenAPITaskFluent.document(String) to automatically detect whether the input is a literal URI or a JQ runtime expression - Added EndpointUtil.isJqExpr() helper method to identify JQ expressions (strings starting with "${" ) - Added comprehensive test coverage with FuncOpenAPITest using MockWebServer - Added serverlessworkflow-impl-openapi dependency to experimental/test module - Updated .gitignore to exclude Bob-Shell notes directory The document() method now intelligently handles both: - Literal URI strings (e.g., "http://example.com/swagger.json") - JQ runtime expressions (e.g., "${.openapi.url}") This improves the developer experience by eliminating the need to choose between document(String) and document(URI) methods based on the input type. Closes #1303 Signed-off-by: Matheus Cruz --- .gitignore | 5 +- experimental/test/pom.xml | 5 + .../fluent/test/FuncOpenAPITest.java | 112 ++++++++++++++++++ .../spec/spi/CallOpenAPITaskFluent.java | 21 +++- .../fluent/spec/spi/EndpointUtil.java | 4 + 5 files changed, 142 insertions(+), 5 deletions(-) create mode 100644 experimental/test/src/test/java/io/serverlessworkflow/fluent/test/FuncOpenAPITest.java diff --git a/.gitignore b/.gitignore index 1dfd7048f..4d08f121e 100644 --- a/.gitignore +++ b/.gitignore @@ -29,4 +29,7 @@ target/ build/ ### VS Code ### -.vscode/ \ No newline at end of file +.vscode/ + +# Bob-Shell +.bob/notes/ \ No newline at end of file diff --git a/experimental/test/pom.xml b/experimental/test/pom.xml index 5661f5822..3a894e76f 100644 --- a/experimental/test/pom.xml +++ b/experimental/test/pom.xml @@ -72,6 +72,11 @@ io.serverlessworkflow serverlessworkflow-impl-jq + + io.serverlessworkflow + serverlessworkflow-impl-openapi + ${project.version} + org.glassfish.jersey.media jersey-media-json-jackson diff --git a/experimental/test/src/test/java/io/serverlessworkflow/fluent/test/FuncOpenAPITest.java b/experimental/test/src/test/java/io/serverlessworkflow/fluent/test/FuncOpenAPITest.java new file mode 100644 index 000000000..dbcdd2073 --- /dev/null +++ b/experimental/test/src/test/java/io/serverlessworkflow/fluent/test/FuncOpenAPITest.java @@ -0,0 +1,112 @@ +/* + * 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.fluent.test; + +import static io.serverlessworkflow.fluent.func.dsl.FuncDSL.openapi; + +import io.serverlessworkflow.fluent.func.FuncWorkflowBuilder; +import io.serverlessworkflow.impl.WorkflowApplication; +import io.serverlessworkflow.impl.WorkflowDefinition; +import io.serverlessworkflow.impl.WorkflowInstance; +import io.serverlessworkflow.impl.WorkflowModel; +import java.io.IOException; +import java.net.URI; +import java.util.Map; +import mockwebserver3.MockResponse; +import mockwebserver3.MockWebServer; +import okhttp3.Headers; +import org.assertj.core.api.SoftAssertions; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +public class FuncOpenAPITest { + + private static MockWebServer mockWebServer; + + @BeforeEach + public void setup() throws IOException { + mockWebServer = new MockWebServer(); + mockWebServer.start(0); + } + + @AfterEach + public void tearDown() { + mockWebServer.close(); + } + + @Test + void test_openapi_document_with_non_jq_uri_string() { + String mockedSwaggerDoc = + """ + { + "swagger": "2.0", + "info": { "version": "1.0.0", "title": "Mock Petstore" }, + "host": "localhost:%d", + "basePath": "/v2", + "schemes": [ "http" ], + "paths": { + "/pet/findByStatus": { + "get": { + "operationId": "findPetsByStatus", + "parameters": [ + { + "name": "status", + "in": "query", + "required": true, + "type": "string" + } + ], + "responses": { "200": { "description": "OK" } } + } + } + } + } + """ + .formatted(mockWebServer.getPort()); + + mockWebServer.enqueue( + new MockResponse(200, Headers.of("Content-Type", "application/json"), mockedSwaggerDoc)); + mockWebServer.enqueue( + new MockResponse( + 200, + Headers.of("Content-Type", "application/json"), + """ + { "description": "OK" } + """)); + var w = + FuncWorkflowBuilder.workflow("openapi-call-workflow") + .tasks( + openapi() + .document(URI.create(mockWebServer.url("/v2/swagger.json").toString())) + .operation("findPetsByStatus") + .parameters(Map.of("status", "available"))) + .build(); + + try (WorkflowApplication app = WorkflowApplication.builder().build()) { + + WorkflowDefinition def = app.workflowDefinition(w); + WorkflowInstance instance = def.instance(Map.of()); + WorkflowModel model = instance.start().join(); + + SoftAssertions.assertSoftly( + softly -> { + softly.assertThat(model).isNotNull(); + softly.assertThat(model.asMap()).contains(Map.of("description", "OK")); + }); + } + } +} diff --git a/fluent/spec/src/main/java/io/serverlessworkflow/fluent/spec/spi/CallOpenAPITaskFluent.java b/fluent/spec/src/main/java/io/serverlessworkflow/fluent/spec/spi/CallOpenAPITaskFluent.java index 48b1a701e..10b0f470c 100644 --- a/fluent/spec/src/main/java/io/serverlessworkflow/fluent/spec/spi/CallOpenAPITaskFluent.java +++ b/fluent/spec/src/main/java/io/serverlessworkflow/fluent/spec/spi/CallOpenAPITaskFluent.java @@ -40,11 +40,24 @@ default CallOpenAPI build() { SELF self(); + /** + * Sets the OpenAPI document location. This method automatically detects whether the provided + * string is a literal URI or a JQ runtime expression. + * + * @param uri the OpenAPI document location as either a literal URI string or a JQ expression + * @return this builder instance for method chaining + * @see #document(URI) for setting a literal URI directly + * @see #document(String, AuthenticationConfigurer) for setting a document with authentication + */ default SELF document(String uri) { - ((CallOpenAPI) this.self().getTask()) - .getWith() - .withDocument( - new ExternalResource().withEndpoint(new Endpoint().withRuntimeExpression(uri))); + if (EndpointUtil.isJqExpr(uri)) { + ((CallOpenAPI) this.self().getTask()) + .getWith() + .withDocument( + new ExternalResource().withEndpoint(new Endpoint().withRuntimeExpression(uri))); + } else { + return document(URI.create(uri)); + } return self(); } diff --git a/fluent/spec/src/main/java/io/serverlessworkflow/fluent/spec/spi/EndpointUtil.java b/fluent/spec/src/main/java/io/serverlessworkflow/fluent/spec/spi/EndpointUtil.java index d1b6a3efa..103f7e966 100644 --- a/fluent/spec/src/main/java/io/serverlessworkflow/fluent/spec/spi/EndpointUtil.java +++ b/fluent/spec/src/main/java/io/serverlessworkflow/fluent/spec/spi/EndpointUtil.java @@ -63,4 +63,8 @@ private static boolean isUrlLike(String value) { } return true; } + + public static boolean isJqExpr(String expr) { + return expr != null && expr.startsWith("${"); + } } From 9ba01318eea56bc7679f1a4923d5228be6e4d155 Mon Sep 17 00:00:00 2001 From: Matheus Cruz Date: Wed, 15 Apr 2026 14:17:27 -0300 Subject: [PATCH 2/3] Apply pull request suggestions Signed-off-by: Matheus Cruz --- experimental/test/pom.xml | 2 ++ .../fluent/spec/spi/CallOpenAPITaskFluent.java | 13 +++---------- .../fluent/spec/spi/EndpointUtil.java | 5 +++-- 3 files changed, 8 insertions(+), 12 deletions(-) diff --git a/experimental/test/pom.xml b/experimental/test/pom.xml index 3a894e76f..6cf53a5cd 100644 --- a/experimental/test/pom.xml +++ b/experimental/test/pom.xml @@ -71,11 +71,13 @@ io.serverlessworkflow serverlessworkflow-impl-jq + test io.serverlessworkflow serverlessworkflow-impl-openapi ${project.version} + test org.glassfish.jersey.media diff --git a/fluent/spec/src/main/java/io/serverlessworkflow/fluent/spec/spi/CallOpenAPITaskFluent.java b/fluent/spec/src/main/java/io/serverlessworkflow/fluent/spec/spi/CallOpenAPITaskFluent.java index 10b0f470c..36ef6baa5 100644 --- a/fluent/spec/src/main/java/io/serverlessworkflow/fluent/spec/spi/CallOpenAPITaskFluent.java +++ b/fluent/spec/src/main/java/io/serverlessworkflow/fluent/spec/spi/CallOpenAPITaskFluent.java @@ -50,14 +50,7 @@ default CallOpenAPI build() { * @see #document(String, AuthenticationConfigurer) for setting a document with authentication */ default SELF document(String uri) { - if (EndpointUtil.isJqExpr(uri)) { - ((CallOpenAPI) this.self().getTask()) - .getWith() - .withDocument( - new ExternalResource().withEndpoint(new Endpoint().withRuntimeExpression(uri))); - } else { - return document(URI.create(uri)); - } + ((CallOpenAPI) this.self().getTask()).getWith().setDocument(EndpointUtil.externalResource(uri)); return self(); } @@ -81,8 +74,8 @@ default SELF document(String uri, AuthenticationConfigurer authenticationConfigu .setDocument( new ExternalResource() .withEndpoint( - new Endpoint() - .withRuntimeExpression(uri) + EndpointUtil.externalResource(uri) + .getEndpoint() .withEndpointConfiguration( new EndpointConfiguration() .withUri(new EndpointUri().withExpressionEndpointURI(uri)) diff --git a/fluent/spec/src/main/java/io/serverlessworkflow/fluent/spec/spi/EndpointUtil.java b/fluent/spec/src/main/java/io/serverlessworkflow/fluent/spec/spi/EndpointUtil.java index 103f7e966..50efe2a9c 100644 --- a/fluent/spec/src/main/java/io/serverlessworkflow/fluent/spec/spi/EndpointUtil.java +++ b/fluent/spec/src/main/java/io/serverlessworkflow/fluent/spec/spi/EndpointUtil.java @@ -16,6 +16,7 @@ package io.serverlessworkflow.fluent.spec.spi; import io.serverlessworkflow.api.types.Endpoint; +import io.serverlessworkflow.api.types.ExternalResource; import io.serverlessworkflow.api.types.UriTemplate; import java.net.URI; import java.util.Objects; @@ -64,7 +65,7 @@ private static boolean isUrlLike(String value) { return true; } - public static boolean isJqExpr(String expr) { - return expr != null && expr.startsWith("${"); + public static ExternalResource externalResource(String expr) { + return new ExternalResource().withEndpoint(fromString(expr)); } } From fd7583dcfb2d97d6a0a4876e74800653cb1785d6 Mon Sep 17 00:00:00 2001 From: fjtirado Date: Thu, 16 Apr 2026 15:59:23 +0200 Subject: [PATCH 3/3] Fix authorization fluent Signed-off-by: fjtirado --- .../fluent/func/FuncDSLTest.java | 24 +++++++- .../fluent/spec/spi/CallHttpTaskFluent.java | 18 +++--- .../spec/spi/CallOpenAPITaskFluent.java | 25 +++----- .../fluent/spec/spi/EndpointUtil.java | 61 ++++++++++++------- .../fluent/spec/dsl/CallHttpAuthDslTest.java | 15 +++-- .../fluent/spec/dsl/CallOpenApiDslTest.java | 8 ++- 6 files changed, 96 insertions(+), 55 deletions(-) diff --git a/experimental/fluent/func/src/test/java/io/serverlessworkflow/fluent/func/FuncDSLTest.java b/experimental/fluent/func/src/test/java/io/serverlessworkflow/fluent/func/FuncDSLTest.java index a11d2c753..a65209770 100644 --- a/experimental/fluent/func/src/test/java/io/serverlessworkflow/fluent/func/FuncDSLTest.java +++ b/experimental/fluent/func/src/test/java/io/serverlessworkflow/fluent/func/FuncDSLTest.java @@ -266,7 +266,13 @@ void get_named_with_authentication_uses_auth_policy() { assertEquals("GET", http.getWith().getMethod()); assertEquals( "http://service/api/users", - http.getWith().getEndpoint().getUriTemplate().getLiteralUri().toString(), + http.getWith() + .getEndpoint() + .getEndpointConfiguration() + .getUri() + .getLiteralEndpointURI() + .getLiteralUri() + .toString(), "endpoint should be set from get(name, endpoint, auth)"); assertNotNull( @@ -371,7 +377,13 @@ void post_named_with_authentication() { assertEquals("POST", http.getWith().getMethod()); assertEquals( "https://orders.example.com/api/orders", - http.getWith().getEndpoint().getUriTemplate().getLiteralUri().toString()); + http.getWith() + .getEndpoint() + .getEndpointConfiguration() + .getUri() + .getLiteralEndpointURI() + .getLiteralUri() + .toString()); assertEquals(body, http.getWith().getBody()); assertNotNull(http.getWith().getEndpoint().getEndpointConfiguration().getAuthentication()); @@ -409,7 +421,13 @@ void call_with_preconfigured_http_spec() { assertEquals("POST", http.getWith().getMethod()); assertEquals( "http://service/api", - http.getWith().getEndpoint().getUriTemplate().getLiteralUri().toString()); + http.getWith() + .getEndpoint() + .getEndpointConfiguration() + .getUri() + .getLiteralEndpointURI() + .getLiteralUri() + .toString()); assertEquals( "svc-auth", http.getWith() diff --git a/fluent/spec/src/main/java/io/serverlessworkflow/fluent/spec/spi/CallHttpTaskFluent.java b/fluent/spec/src/main/java/io/serverlessworkflow/fluent/spec/spi/CallHttpTaskFluent.java index 61cf0db6e..89ca52fd0 100644 --- a/fluent/spec/src/main/java/io/serverlessworkflow/fluent/spec/spi/CallHttpTaskFluent.java +++ b/fluent/spec/src/main/java/io/serverlessworkflow/fluent/spec/spi/CallHttpTaskFluent.java @@ -84,23 +84,21 @@ default SELF endpoint(String expr, Consumer= 0 || trimmed.indexOf('}') >= 0) { - template.setLiteralUriTemplate(trimmed); - } else { - template.setLiteralUri(URI.create(trimmed)); - } - endpoint.setUriTemplate(template); - return endpoint; - } - // Let the runtime engine to verify if it's a valid jq expression since ${} it's not the only - // way of checking it. - endpoint.setRuntimeExpression(expr); + if (auth != null) { + endpoint.setEndpointConfiguration( + new EndpointConfiguration(buildEndpointUri(trimmed)).withAuthentication(auth)); + } else if (isUrlLike(trimmed)) { + endpoint.setUriTemplate(buildUriTemplate(trimmed)); + } else { + // Let the runtime engine to verify if it's a valid jq expression since ${} it's not the only + // way of checking it. + endpoint.setRuntimeExpression(uri); + } return endpoint; } + private static EndpointUri buildEndpointUri(String uri) { + EndpointUri endpointUri = new EndpointUri(); + if (isUrlLike(uri)) { + endpointUri.setLiteralEndpointURI(buildUriTemplate(uri)); + } else { + endpointUri.setExpressionEndpointURI(uri); + } + return endpointUri; + } + + private static UriTemplate buildUriTemplate(String trimmed) { + UriTemplate template = new UriTemplate(); + if (trimmed.indexOf('{') >= 0 || trimmed.indexOf('}') >= 0) { + template.setLiteralUriTemplate(trimmed); + } else { + template.setLiteralUri(URI.create(trimmed)); + } + return template; + } + private static boolean isUrlLike(String value) { // same idea as UriTemplate.literalUriTemplate_Pattern: ^[A-Za-z][A-Za-z0-9+\\-.]*://.* int idx = value.indexOf("://"); @@ -64,8 +87,4 @@ private static boolean isUrlLike(String value) { } return true; } - - public static ExternalResource externalResource(String expr) { - return new ExternalResource().withEndpoint(fromString(expr)); - } } diff --git a/fluent/spec/src/test/java/io/serverlessworkflow/fluent/spec/dsl/CallHttpAuthDslTest.java b/fluent/spec/src/test/java/io/serverlessworkflow/fluent/spec/dsl/CallHttpAuthDslTest.java index 874cce3b5..94fedc450 100644 --- a/fluent/spec/src/test/java/io/serverlessworkflow/fluent/spec/dsl/CallHttpAuthDslTest.java +++ b/fluent/spec/src/test/java/io/serverlessworkflow/fluent/spec/dsl/CallHttpAuthDslTest.java @@ -47,7 +47,8 @@ void when_call_http_with_basic_auth_on_endpoint_expr() { assertThat(wf.getDo().get(0).getTask().getCallTask().get()).isNotNull(); // Endpoint expression is set - assertThat(args.getEndpoint().getRuntimeExpression()).isEqualTo(EXPR_ENDPOINT); + assertThat(args.getEndpoint().getEndpointConfiguration().getUri().getExpressionEndpointURI()) + .isEqualTo(EXPR_ENDPOINT); // Auth populated: BASIC (others null) var auth = @@ -83,7 +84,8 @@ void when_call_http_with_bearer_auth_on_endpoint_expr() { var args = wf.getDo().get(0).getTask().getCallTask().getCallHTTP().getWith(); - assertThat(args.getEndpoint().getRuntimeExpression()).isEqualTo(EXPR_ENDPOINT); + assertThat(args.getEndpoint().getEndpointConfiguration().getUri().getExpressionEndpointURI()) + .isEqualTo(EXPR_ENDPOINT); var auth = args.getEndpoint().getEndpointConfiguration().getAuthentication().getAuthenticationPolicy(); @@ -112,7 +114,8 @@ void when_call_http_with_digest_auth_on_endpoint_expr() { var args = wf.getDo().get(0).getTask().getCallTask().getCallHTTP().getWith(); - assertThat(args.getEndpoint().getRuntimeExpression()).isEqualTo(EXPR_ENDPOINT); + assertThat(args.getEndpoint().getEndpointConfiguration().getUri().getExpressionEndpointURI()) + .isEqualTo(EXPR_ENDPOINT); var auth = args.getEndpoint().getEndpointConfiguration().getAuthentication().getAuthenticationPolicy(); @@ -158,7 +161,8 @@ void when_call_http_with_oidc_auth_on_endpoint_expr_with_client() { var args = wf.getDo().get(0).getTask().getCallTask().getCallHTTP().getWith(); - assertThat(args.getEndpoint().getRuntimeExpression()).isEqualTo(EXPR_ENDPOINT); + assertThat(args.getEndpoint().getEndpointConfiguration().getUri().getExpressionEndpointURI()) + .isEqualTo(EXPR_ENDPOINT); var auth = args.getEndpoint().getEndpointConfiguration().getAuthentication().getAuthenticationPolicy(); @@ -203,7 +207,8 @@ void when_call_http_with_oauth2_alias_on_endpoint_expr_without_client() { var args = wf.getDo().get(0).getTask().getCallTask().getCallHTTP().getWith(); - assertThat(args.getEndpoint().getRuntimeExpression()).isEqualTo(EXPR_ENDPOINT); + assertThat(args.getEndpoint().getEndpointConfiguration().getUri().getExpressionEndpointURI()) + .isEqualTo(EXPR_ENDPOINT); var auth = args.getEndpoint().getEndpointConfiguration().getAuthentication().getAuthenticationPolicy(); diff --git a/fluent/spec/src/test/java/io/serverlessworkflow/fluent/spec/dsl/CallOpenApiDslTest.java b/fluent/spec/src/test/java/io/serverlessworkflow/fluent/spec/dsl/CallOpenApiDslTest.java index 70a91f282..fc2cc4b6a 100644 --- a/fluent/spec/src/test/java/io/serverlessworkflow/fluent/spec/dsl/CallOpenApiDslTest.java +++ b/fluent/spec/src/test/java/io/serverlessworkflow/fluent/spec/dsl/CallOpenApiDslTest.java @@ -55,7 +55,13 @@ void when_call_openapi_with_basic_auth_on_document_expr() { // Document and endpoint expression assertThat(with.getDocument()).isNotNull(); assertThat(with.getDocument().getEndpoint()).isNotNull(); - assertThat(with.getDocument().getEndpoint().getRuntimeExpression()).isEqualTo(EXPR_DOCUMENT); + assertThat( + with.getDocument() + .getEndpoint() + .getEndpointConfiguration() + .getUri() + .getExpressionEndpointURI()) + .isEqualTo(EXPR_DOCUMENT); // Endpoint configuration URI expression var endpointConfig = with.getDocument().getEndpoint().getEndpointConfiguration();