From 124653c251995fbe0047b5cfbe3fe31bebd5d1c0 Mon Sep 17 00:00:00 2001 From: Matheus Cruz Date: Thu, 16 Apr 2026 18:22:19 -0300 Subject: [PATCH] Refactor how endpoint and authentication are handled in the fluent API Signed-off-by: Matheus Cruz --- .gitignore | 5 +- .../fluent/func/FuncDSLTest.java | 32 ++++- experimental/test/pom.xml | 7 ++ .../fluent/test/FuncOpenAPITest.java | 112 ++++++++++++++++++ .../fluent/spec/spi/CallHttpTaskFluent.java | 60 +++++++--- .../spec/spi/CallOpenAPITaskFluent.java | 33 +++--- .../fluent/spec/spi/EndpointUtil.java | 56 ++++++--- .../fluent/spec/dsl/CallHttpAuthDslTest.java | 23 +++- .../fluent/spec/dsl/CallOpenApiDslTest.java | 8 +- 9 files changed, 278 insertions(+), 58 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/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..f7c68be9e 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( @@ -304,7 +310,13 @@ void get_with_uri_and_authentication() { assertEquals("GET", http.getWith().getMethod()); assertEquals( endpoint.toString(), - http.getWith().getEndpoint().getUriTemplate().getLiteralUri().toString(), + http.getWith() + .getEndpoint() + .getEndpointConfiguration() + .getUri() + .getLiteralEndpointURI() + .getLiteralUri() + .toString(), "endpoint should be derived from URI"); assertNotNull(http.getWith().getEndpoint().getEndpointConfiguration().getAuthentication()); @@ -371,7 +383,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 +427,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/experimental/test/pom.xml b/experimental/test/pom.xml index 5661f5822..6cf53a5cd 100644 --- a/experimental/test/pom.xml +++ b/experimental/test/pom.xml @@ -71,6 +71,13 @@ io.serverlessworkflow serverlessworkflow-impl-jq + test + + + io.serverlessworkflow + serverlessworkflow-impl-openapi + ${project.version} + test org.glassfish.jersey.media 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/CallHttpTaskFluent.java b/fluent/spec/src/main/java/io/serverlessworkflow/fluent/spec/spi/CallHttpTaskFluent.java index 61cf0db6e..fac512312 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 @@ -50,6 +50,12 @@ default SELF method(String method) { return self(); } + /** + * Sets the endpoint URI for the HTTP call. + * + * @param endpoint the URI to call + * @return this builder instance for method chaining + */ default SELF endpoint(URI endpoint) { ((CallHTTP) this.self().getTask()) .getWith() @@ -57,10 +63,16 @@ default SELF endpoint(URI endpoint) { return self(); } + /** + * Sets the endpoint URI for the HTTP call with authentication configuration. + * + * @param endpoint the URI to call + * @param auth consumer to configure authentication policy + * @return this builder instance for method chaining + */ default SELF endpoint(URI endpoint, Consumer auth) { final ReferenceableAuthenticationPolicyBuilder policy = new ReferenceableAuthenticationPolicyBuilder(); - final UriTemplate uriTemplate = new UriTemplate().withLiteralUri(endpoint); auth.accept(policy); ((CallHTTP) this.self().getTask()) .getWith() @@ -68,39 +80,59 @@ default SELF endpoint(URI endpoint, Consumer auth) { final ReferenceableAuthenticationPolicyBuilder policy = new ReferenceableAuthenticationPolicyBuilder(); auth.accept(policy); - final Endpoint endpoint = EndpointUtil.fromString(expr); - endpoint.setEndpointConfiguration( - new EndpointConfiguration().withAuthentication(policy.build())); - - ((CallHTTP) this.self().getTask()).getWith().setEndpoint(endpoint); + ((CallHTTP) this.self().getTask()) + .getWith() + .setEndpoint(EndpointUtil.fromString(expr, policy.build())); return self(); } + /** + * Sets the endpoint using a runtime expression or URI string with authentication policy + * reference. + * + * @param expr the runtime expression or URI string for the endpoint + * @param authUse the name of the authentication policy to reference + * @return this builder instance for method chaining + */ default SELF endpoint(String expr, String authUse) { - final Endpoint endpoint = EndpointUtil.fromString(expr); - endpoint.withEndpointConfiguration( - new EndpointConfiguration() - .withAuthentication( + ((CallHTTP) this.self().getTask()) + .getWith() + .setEndpoint( + EndpointUtil.fromString( + expr, new ReferenceableAuthenticationPolicy() .withAuthenticationPolicyReference( new AuthenticationPolicyReference(authUse)))); - ((CallHTTP) this.self().getTask()).getWith().setEndpoint(endpoint); return self(); } 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..2149e4649 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 @@ -21,6 +21,7 @@ import io.serverlessworkflow.api.types.EndpointUri; import io.serverlessworkflow.api.types.ExternalResource; import io.serverlessworkflow.api.types.OpenAPIArguments; +import io.serverlessworkflow.api.types.ReferenceableAuthenticationPolicy; import io.serverlessworkflow.api.types.UriTemplate; import io.serverlessworkflow.fluent.spec.ReferenceableAuthenticationPolicyBuilder; import io.serverlessworkflow.fluent.spec.TaskBaseBuilder; @@ -40,11 +41,19 @@ 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))); + .setDocument(new ExternalResource().withEndpoint(EndpointUtil.fromString(uri))); return self(); } @@ -62,18 +71,11 @@ default SELF document(String uri, AuthenticationConfigurer authenticationConfigu final ReferenceableAuthenticationPolicyBuilder policy = new ReferenceableAuthenticationPolicyBuilder(); authenticationConfigurer.accept(policy); - ((CallOpenAPI) this.self().getTask()).getWith().setAuthentication(policy.build()); + ReferenceableAuthenticationPolicy auth = policy.build(); + ((CallOpenAPI) this.self().getTask()).getWith().setAuthentication(auth); ((CallOpenAPI) this.self().getTask()) .getWith() - .setDocument( - new ExternalResource() - .withEndpoint( - new Endpoint() - .withRuntimeExpression(uri) - .withEndpointConfiguration( - new EndpointConfiguration() - .withUri(new EndpointUri().withExpressionEndpointURI(uri)) - .withAuthentication(policy.build())))); + .setDocument(new ExternalResource().withEndpoint(EndpointUtil.fromString(uri, auth))); return self(); } @@ -81,22 +83,21 @@ default SELF document(URI uri, AuthenticationConfigurer authenticationConfigurer final ReferenceableAuthenticationPolicyBuilder policy = new ReferenceableAuthenticationPolicyBuilder(); authenticationConfigurer.accept(policy); - - ((CallOpenAPI) this.self().getTask()).getWith().setAuthentication(policy.build()); + ReferenceableAuthenticationPolicy auth = policy.build(); + ((CallOpenAPI) this.self().getTask()).getWith().setAuthentication(auth); ((CallOpenAPI) this.self().getTask()) .getWith() .setDocument( new ExternalResource() .withEndpoint( new Endpoint() - .withUriTemplate(new UriTemplate().withLiteralUri(uri)) .withEndpointConfiguration( new EndpointConfiguration() .withUri( new EndpointUri() .withLiteralEndpointURI( new UriTemplate().withLiteralUri(uri))) - .withAuthentication(policy.build())))); + .withAuthentication(auth)))); 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..f13db07e7 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,9 @@ package io.serverlessworkflow.fluent.spec.spi; import io.serverlessworkflow.api.types.Endpoint; +import io.serverlessworkflow.api.types.EndpointConfiguration; +import io.serverlessworkflow.api.types.EndpointUri; +import io.serverlessworkflow.api.types.ReferenceableAuthenticationPolicy; import io.serverlessworkflow.api.types.UriTemplate; import java.net.URI; import java.util.Objects; @@ -24,27 +27,48 @@ public final class EndpointUtil { private EndpointUtil() {} - public static Endpoint fromString(String expr) { - Objects.requireNonNull(expr, "Endpoint expression cannot be null"); - String trimmed = expr.trim(); + public static Endpoint fromString(String uri) { + return fromString(uri, null); + } + + public static Endpoint fromString(String uri, ReferenceableAuthenticationPolicy auth) { + Objects.requireNonNull(uri, "Endpoint URI cannot be null"); + String trimmed = uri.trim(); Endpoint endpoint = new Endpoint(); - if (isUrlLike(trimmed)) { - UriTemplate template = new UriTemplate(); - if (trimmed.indexOf('{') >= 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("://"); 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..211f64d0f 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(); @@ -237,7 +242,13 @@ void when_call_http_with_basic_auth_on_uri_string() { var args = wf.getDo().get(0).getTask().getCallTask().getCallHTTP().getWith(); - assertThat(args.getEndpoint().getUriTemplate().getLiteralUri().toString()) + assertThat( + args.getEndpoint() + .getEndpointConfiguration() + .getUri() + .getLiteralEndpointURI() + .getLiteralUri() + .toString()) .isEqualTo("https://api.example.com/v1/resource"); var auth = 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();