diff --git a/docs/modules/ROOT/examples/org/acme/ConditionalWorkflow.java b/docs/modules/ROOT/examples/org/acme/ConditionalWorkflow.java new file mode 100644 index 000000000..cf6b58de2 --- /dev/null +++ b/docs/modules/ROOT/examples/org/acme/ConditionalWorkflow.java @@ -0,0 +1,29 @@ +package org.acme; + +import static io.serverlessworkflow.fluent.func.dsl.FuncDSL.*; + +import jakarta.enterprise.context.ApplicationScoped; + +import io.quarkiverse.flow.Flow; +import io.serverlessworkflow.api.types.FlowDirectiveEnum; +import io.serverlessworkflow.api.types.Workflow; +import io.serverlessworkflow.fluent.func.FuncWorkflowBuilder; + +@ApplicationScoped +public class ConditionalWorkflow extends Flow { + @Override + public Workflow descriptor() { + return FuncWorkflowBuilder.workflow("conditional-routing") + .tasks( + // 1. Evaluate the condition and branch + switchWhenOrElse((ScorePayload p) -> p.score() >= 80, "approveTask", "rejectTask"), + + // 2. Branch A: Score is 80 or higher + post("approveTask", "", "http://localhost:8089/approve") + .then(FlowDirectiveEnum.END), // equals to break; in switch cases + + // 3. Branch B: Score is below 80 + post("rejectTask", "", "http://localhost:8089/reject")) + .build(); + } +} diff --git a/docs/modules/ROOT/examples/org/acme/ContextWorkflow.java b/docs/modules/ROOT/examples/org/acme/ContextWorkflow.java new file mode 100644 index 000000000..f85fb1742 --- /dev/null +++ b/docs/modules/ROOT/examples/org/acme/ContextWorkflow.java @@ -0,0 +1,24 @@ +package org.acme; + +import static io.serverlessworkflow.fluent.func.dsl.FuncDSL.withContext; + +import jakarta.enterprise.context.ApplicationScoped; + +import io.quarkiverse.flow.Flow; +import io.serverlessworkflow.api.types.Workflow; +import io.serverlessworkflow.fluent.func.FuncWorkflowBuilder; +import io.serverlessworkflow.impl.WorkflowContextData; + +@ApplicationScoped +public class ContextWorkflow extends Flow { + @Override + public Workflow descriptor() { + return FuncWorkflowBuilder.workflow("context-aware") + .tasks( + withContext((String input, WorkflowContextData contextData) -> { + System.out.println("Instance ID: " + contextData.instanceData().id()); + return "Processed " + input; + }, String.class)) + .build(); + } +} diff --git a/docs/modules/ROOT/examples/org/acme/CronWorkflow.java b/docs/modules/ROOT/examples/org/acme/CronWorkflow.java new file mode 100644 index 000000000..c9d72d4e9 --- /dev/null +++ b/docs/modules/ROOT/examples/org/acme/CronWorkflow.java @@ -0,0 +1,25 @@ +package org.acme; + +// Static imports recommended for brevity: +import static io.serverlessworkflow.fluent.func.dsl.FuncDSL.*; + +import java.util.Date; +import java.util.Map; + +import jakarta.enterprise.context.ApplicationScoped; + +import io.quarkiverse.flow.Flow; +import io.serverlessworkflow.api.types.Workflow; +import io.serverlessworkflow.fluent.func.FuncWorkflowBuilder; + +@ApplicationScoped +public class CronWorkflow extends Flow { + @Override + public Workflow descriptor() { + return FuncWorkflowBuilder.workflow("cron-workflow") + .schedule(cron("* * * * * ?")) // Every second + .tasks( + set(Map.of("message", "Executed Cron Workflow at: " + new Date()))) + .build(); + } +} diff --git a/docs/modules/ROOT/examples/org/acme/EmitWorkflow.java b/docs/modules/ROOT/examples/org/acme/EmitWorkflow.java new file mode 100644 index 000000000..964f0de48 --- /dev/null +++ b/docs/modules/ROOT/examples/org/acme/EmitWorkflow.java @@ -0,0 +1,20 @@ +package org.acme; + +import static io.serverlessworkflow.fluent.func.dsl.FuncDSL.emitJson; + +import jakarta.enterprise.context.ApplicationScoped; + +import io.quarkiverse.flow.Flow; +import io.serverlessworkflow.api.types.Workflow; +import io.serverlessworkflow.fluent.func.FuncWorkflowBuilder; + +@ApplicationScoped +public class EmitWorkflow extends Flow { + @Override + public Workflow descriptor() { + return FuncWorkflowBuilder.workflow("emit-event-workflow", "org.acme", "1.0") + .tasks( + emitJson("orderPlaced", "com.petstore.order.placed.v1", Message.class)) + .build(); + } +} diff --git a/docs/modules/ROOT/examples/org/acme/ExampleEvent.java b/docs/modules/ROOT/examples/org/acme/ExampleEvent.java new file mode 100644 index 000000000..69131a545 --- /dev/null +++ b/docs/modules/ROOT/examples/org/acme/ExampleEvent.java @@ -0,0 +1,5 @@ +package org.acme; + +public record ExampleEvent(String eventName) { + +} diff --git a/docs/modules/ROOT/examples/org/acme/ExampleWorkflowsWireMockResource.java b/docs/modules/ROOT/examples/org/acme/ExampleWorkflowsWireMockResource.java new file mode 100644 index 000000000..c15cef28d --- /dev/null +++ b/docs/modules/ROOT/examples/org/acme/ExampleWorkflowsWireMockResource.java @@ -0,0 +1,141 @@ +package org.acme; + +import static com.github.tomakehurst.wiremock.client.WireMock.*; + +import java.util.Map; + +import com.github.tomakehurst.wiremock.WireMockServer; + +import io.quarkus.test.common.QuarkusTestResourceLifecycleManager; + +public class ExampleWorkflowsWireMockResource implements QuarkusTestResourceLifecycleManager { + + private WireMockServer wireMockServer; + + @Override + public Map start() { + // Start WireMock on the fixed port expected by the example workflows + wireMockServer = new WireMockServer(8089); + wireMockServer.start(); + + // --------------------------------------------------------- + // STUBS FOR HTTP WORKFLOW TESTS + // --------------------------------------------------------- + wireMockServer.stubFor(get(urlEqualTo("/api/people?search=luke")) + .withHeader("Accept", equalTo("application/json")) + .willReturn(aResponse() + .withHeader("Content-Type", "application/json") + .withBody(""" + { + "count": 1, + "results": [ + { + "name": "Luke Skywalker Mock" + } + ] + } + """))); + + // --------------------------------------------------------- + // STUBS FOR OPENAPI WORKFLOW TESTS + // --------------------------------------------------------- + + // 1. Stub the Swagger Document + String mockedSwaggerDoc = """ + { + "swagger": "2.0", + "info": { "version": "1.0.0", "title": "Mock Petstore" }, + "host": "localhost:8089", + "basePath": "/v2", + "schemes": [ "http" ], + "paths": { + "/pet/findByStatus": { + "get": { + "operationId": "findPetsByStatus", + "parameters": [ + { + "name": "status", + "in": "query", + "required": true, + "type": "string" + } + ], + "responses": { "200": { "description": "OK" } } + } + } + } + } + """; + + // Use any as the workflow first query with HEAD and the GET + wireMockServer.stubFor(any(urlEqualTo("/v2/swagger.json")) + .willReturn(aResponse() + .withHeader("Content-Type", "application/json") + .withBody(mockedSwaggerDoc))); + + // 2. Stub the actual Petstore Endpoint defined in the document above + wireMockServer.stubFor(get(urlPathEqualTo("/v2/pet/findByStatus")) + .withQueryParam("status", equalTo("available")) + .willReturn(aResponse() + .withHeader("Content-Type", "application/json") + // The Petstore API returns a JSON array + .withBody("[{\"id\": 101, \"name\": \"Mocked Doggo\", \"status\": \"available\"}]"))); + + // --------------------------------------------------------- + // 3. STUB FOR LISTEN WORKFLOW (Event Wakeup) + // --------------------------------------------------------- + wireMockServer.stubFor(post(urlEqualTo("/start")) + .willReturn(aResponse() + .withStatus(200) + .withHeader("Content-Type", "application/json") + .withBody("{\"status\": \"started successfully\"}"))); + + // --------------------------------------------------------- + // 4. STUBS FOR CONDITIONAL WORKFLOW + // --------------------------------------------------------- + wireMockServer.stubFor(post(urlEqualTo("/approve")) + .willReturn(aResponse() + .withStatus(200) + .withHeader("Content-Type", "application/json") + .withBody("{}"))); + + wireMockServer.stubFor(post(urlEqualTo("/reject")) + .willReturn(aResponse() + .withStatus(200) + .withHeader("Content-Type", "application/json") + .withBody("{}"))); + + // --------------------------------------------------------- + // 5. STUBS FOR ITERATION WORKFLOW + // --------------------------------------------------------- + wireMockServer.stubFor(post(urlEqualTo("/process-order")) + .willReturn(aResponse() + .withStatus(200) + .withHeader("Content-Type", "application/json") + .withBody("{\"processed_orders_status\": \"success\"}"))); + + // --------------------------------------------------------- + // 7. STUBS FOR PARALLEL WORKFLOW + // --------------------------------------------------------- + wireMockServer.stubFor(post(urlEqualTo("/inventory-check")) + .willReturn(aResponse() + .withStatus(200) + .withHeader("Content-Type", "application/json") + .withBody("{}"))); + + wireMockServer.stubFor(post(urlEqualTo("/credit-check")) + .willReturn(aResponse() + .withStatus(200) + .withHeader("Content-Type", "application/json") + .withBody("{}"))); + + return Map.of(); // No properties to override + } + + @Override + public void stop() { + if (wireMockServer != null) { + wireMockServer.stop(); + } + } +} diff --git a/docs/modules/ROOT/examples/org/acme/ForEachWorkflow.java b/docs/modules/ROOT/examples/org/acme/ForEachWorkflow.java new file mode 100644 index 000000000..31d22f6b6 --- /dev/null +++ b/docs/modules/ROOT/examples/org/acme/ForEachWorkflow.java @@ -0,0 +1,24 @@ +package org.acme; + +import static io.serverlessworkflow.fluent.func.dsl.FuncDSL.*; + +import jakarta.enterprise.context.ApplicationScoped; + +import io.quarkiverse.flow.Flow; +import io.serverlessworkflow.api.types.Workflow; +import io.serverlessworkflow.fluent.func.FuncWorkflowBuilder; + +@ApplicationScoped +public class ForEachWorkflow extends Flow { + @Override + public Workflow descriptor() { + return FuncWorkflowBuilder.workflow("foreach-workflow") + .tasks( + forEach(OrdersPayload::orders, + tasks( + post("$item.id", + "http://localhost:8089/process-order") + .exportAsTaskOutput()))) + .build(); + } +} diff --git a/docs/modules/ROOT/examples/org/acme/HttpOauth2Workflow.java b/docs/modules/ROOT/examples/org/acme/HttpOauth2Workflow.java new file mode 100644 index 000000000..43e159cf0 --- /dev/null +++ b/docs/modules/ROOT/examples/org/acme/HttpOauth2Workflow.java @@ -0,0 +1,32 @@ +package org.acme; + +import static io.serverlessworkflow.fluent.func.dsl.FuncDSL.*; +import static io.serverlessworkflow.fluent.func.dsl.FuncDSL.oauth2; + +import jakarta.enterprise.context.ApplicationScoped; + +import io.quarkiverse.flow.Flow; +import io.serverlessworkflow.api.types.OAuth2AuthenticationData; +import io.serverlessworkflow.api.types.Workflow; +import io.serverlessworkflow.fluent.func.FuncWorkflowBuilder; + +@ApplicationScoped +public class HttpOauth2Workflow extends Flow { + @Override + public Workflow descriptor() { + return FuncWorkflowBuilder.workflow("oauth2-authentication-workflow") + .tasks( + call("getPets", + http() + .GET() + .query("petId", "${ .petId }") + .uri("http://localhost:8090/v2/pet", + oauth2("http://localhost:8090/realms/fake-authority", + OAuth2AuthenticationData.OAuth2AuthenticationDataGrant.CLIENT_CREDENTIALS, + "workflow-runtime-id", + "workflow-runtime-secret")) + + )) + .build(); + } +} diff --git a/docs/modules/ROOT/examples/org/acme/HttpWorkflow.java b/docs/modules/ROOT/examples/org/acme/HttpWorkflow.java new file mode 100644 index 000000000..de6e652f7 --- /dev/null +++ b/docs/modules/ROOT/examples/org/acme/HttpWorkflow.java @@ -0,0 +1,31 @@ +package org.acme; + +// Static imports recommended for brevity: +import static io.serverlessworkflow.fluent.func.dsl.FuncDSL.*; +import static io.serverlessworkflow.fluent.func.dsl.FuncDSL.call; + +import jakarta.enterprise.context.ApplicationScoped; + +import io.quarkiverse.flow.Flow; +import io.serverlessworkflow.api.types.Workflow; +import io.serverlessworkflow.fluent.func.FuncWorkflowBuilder; + +@ApplicationScoped +public class HttpWorkflow extends Flow { + @Override + public Workflow descriptor() { + return FuncWorkflowBuilder.workflow("http-with-query-headers", "org.acme", "1.0") + .tasks( + call("searchStarWarsCharacters", + http() + .GET() + // search value is taken from workflow input, jq expression is used + .query("search", "${ .searchQuery }") + .endpoint("http://localhost:8089/api/people") + // Accept value is taken from workflow input, jq expression is used + .header("Accept", "${ .acceptHeaderValue }") + // export the results of the GET request as taskOutput + .exportAsTaskOutput())) + .build(); + } +} diff --git a/docs/modules/ROOT/examples/org/acme/ListenWorkflow.java b/docs/modules/ROOT/examples/org/acme/ListenWorkflow.java new file mode 100644 index 000000000..6ceee29b0 --- /dev/null +++ b/docs/modules/ROOT/examples/org/acme/ListenWorkflow.java @@ -0,0 +1,24 @@ +package org.acme; + +import static io.serverlessworkflow.fluent.func.dsl.FuncDSL.*; + +import jakarta.enterprise.context.ApplicationScoped; + +import io.quarkiverse.flow.Flow; +import io.serverlessworkflow.api.types.Workflow; +import io.serverlessworkflow.fluent.func.FuncWorkflowBuilder; + +@ApplicationScoped +public class ListenWorkflow extends Flow { + @Override + public Workflow descriptor() { + return FuncWorkflowBuilder.workflow("listen-to-one-workflow") + .tasks( + // The workflow will pause here until the engine receives this specific CloudEvent + listen("waitForStartup", toOne("race.started.v1")), + + // Once awakened, it executes this HTTP call + call("startup", post("", "http://localhost:8089/start"))) + .build(); + } +} diff --git a/docs/modules/ROOT/examples/org/acme/OpenApiWorkflow.java b/docs/modules/ROOT/examples/org/acme/OpenApiWorkflow.java new file mode 100644 index 000000000..5bf83ce4e --- /dev/null +++ b/docs/modules/ROOT/examples/org/acme/OpenApiWorkflow.java @@ -0,0 +1,26 @@ +package org.acme; + +import static io.serverlessworkflow.fluent.func.dsl.FuncDSL.*; + +// Static imports recommended for brevity: +import java.util.Map; + +import jakarta.enterprise.context.ApplicationScoped; + +import io.quarkiverse.flow.Flow; +import io.serverlessworkflow.api.types.Workflow; +import io.serverlessworkflow.fluent.func.FuncWorkflowBuilder; + +@ApplicationScoped +public class OpenApiWorkflow extends Flow { + @Override + public Workflow descriptor() { + return FuncWorkflowBuilder.workflow("openapi-call-workflow") + .tasks( + openapi() + .document("http://localhost:8089/v2/swagger.json") + .operation("findPetsByStatus") + .parameters(Map.of("status", "available"))) + .build(); + } +} diff --git a/docs/modules/ROOT/examples/org/acme/Order.java b/docs/modules/ROOT/examples/org/acme/Order.java new file mode 100644 index 000000000..adcef171e --- /dev/null +++ b/docs/modules/ROOT/examples/org/acme/Order.java @@ -0,0 +1,5 @@ +package org.acme; + +public record Order(String id) { + +} diff --git a/docs/modules/ROOT/examples/org/acme/OrdersPayload.java b/docs/modules/ROOT/examples/org/acme/OrdersPayload.java new file mode 100644 index 000000000..654a73d2f --- /dev/null +++ b/docs/modules/ROOT/examples/org/acme/OrdersPayload.java @@ -0,0 +1,6 @@ +package org.acme; + +import java.util.List; + +public record OrdersPayload(List orders) { +} diff --git a/docs/modules/ROOT/examples/org/acme/ParallelWorkflow.java b/docs/modules/ROOT/examples/org/acme/ParallelWorkflow.java new file mode 100644 index 000000000..3c831ab68 --- /dev/null +++ b/docs/modules/ROOT/examples/org/acme/ParallelWorkflow.java @@ -0,0 +1,28 @@ +package org.acme; + +import static io.serverlessworkflow.fluent.func.dsl.FuncDSL.*; + +import jakarta.enterprise.context.ApplicationScoped; + +import io.quarkiverse.flow.Flow; +import io.serverlessworkflow.api.types.Workflow; +import io.serverlessworkflow.fluent.func.FuncWorkflowBuilder; + +@ApplicationScoped +public class ParallelWorkflow extends Flow { + @Override + public Workflow descriptor() { + return FuncWorkflowBuilder.workflow("parallel-workflow-using-branches") + .tasks( + fork("checkInventoryAndCredit", + http("checkInventory") + .method("POST") + .body("") + .endpoint("http://localhost:8089/inventory-check"), + http("checkCredit") + .method("POST") + .body("") + .endpoint("http://localhost:8089/credit-check"))) + .build(); + } +} diff --git a/docs/modules/ROOT/examples/org/acme/ParentWorkflow.java b/docs/modules/ROOT/examples/org/acme/ParentWorkflow.java new file mode 100644 index 000000000..cc2b6e123 --- /dev/null +++ b/docs/modules/ROOT/examples/org/acme/ParentWorkflow.java @@ -0,0 +1,29 @@ +package org.acme; + +import static io.serverlessworkflow.fluent.func.dsl.FuncDSL.*; + +// Static imports recommended for brevity: +import jakarta.enterprise.context.ApplicationScoped; + +import io.quarkiverse.flow.Flow; +import io.serverlessworkflow.api.types.Workflow; +import io.serverlessworkflow.fluent.func.FuncWorkflowBuilder; + +@ApplicationScoped +public class ParentWorkflow extends Flow { + @Override + public Workflow descriptor() { + return FuncWorkflowBuilder.workflow("parent-workflow-with-children", "org.acme", "1.0") + .tasks( + // Using workflow(...) shortcut to reference existing workflow + subflow("executeHttpWorkflow", + workflow("org.acme", "http-with-query-headers", "1.0")), + // Using Consumer to reference existing workflow + subflow("emitEventSubflow", + configurer -> configurer.workflow() + .withName("emit-event-workflow") + .withNamespace("org.acme") + .withVersion("1.0"))) + .build(); + } +} diff --git a/docs/modules/ROOT/examples/org/acme/ScorePayload.java b/docs/modules/ROOT/examples/org/acme/ScorePayload.java new file mode 100644 index 000000000..acc7512c6 --- /dev/null +++ b/docs/modules/ROOT/examples/org/acme/ScorePayload.java @@ -0,0 +1,5 @@ +package org.acme; + +public record ScorePayload(int score) { + +} diff --git a/docs/modules/ROOT/examples/org/acme/SecureWireMockResource.java b/docs/modules/ROOT/examples/org/acme/SecureWireMockResource.java new file mode 100644 index 000000000..536eaa704 --- /dev/null +++ b/docs/modules/ROOT/examples/org/acme/SecureWireMockResource.java @@ -0,0 +1,75 @@ +package org.acme; + +import static com.github.tomakehurst.wiremock.client.WireMock.*; + +import java.util.Map; + +import com.github.tomakehurst.wiremock.WireMockServer; +import com.github.tomakehurst.wiremock.common.ConsoleNotifier; +import com.github.tomakehurst.wiremock.core.WireMockConfiguration; + +import io.quarkus.test.common.QuarkusTestResourceLifecycleManager; + +public class SecureWireMockResource implements QuarkusTestResourceLifecycleManager { + + private WireMockServer wireMockServer; + + private static final String FAKE_JWT = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJ3b3JrZmxvdy1ydW50aW1lLWlkIiwiZXhwIjoxOTk5OTk5OTk5fQ.dummy-signature"; + + @Override + public Map start() { + // 1. Enable WireMock's built-in verbose logging using ConsoleNotifier + WireMockConfiguration config = WireMockConfiguration.wireMockConfig() + .port(8090) + .notifier(new ConsoleNotifier(true)); + + wireMockServer = new WireMockServer(config); + + // 2. Add a custom Request Listener for highly visible output in your test logs + wireMockServer.addMockServiceRequestListener((request, response) -> { + System.out.println("\n====== 🚦 WIREMOCK INTERCEPTED REQUEST 🚦 ======"); + System.out.println("Method : " + request.getMethod()); + System.out.println("URL : " + request.getUrl()); + if (request.containsHeader("Authorization")) { + System.out.println("Auth : " + request.header("Authorization").firstValue()); + } + System.out.println("Status : " + response.getStatus()); + System.out.println("================================================\n"); + }); + + wireMockServer.start(); + + // 3. Mock Keycloak returning the valid JWT format + wireMockServer.stubFor(post(urlMatching("/realms/fake-authority.*")) + .willReturn(aResponse() + .withHeader("Content-Type", "application/json") + .withBody(""" + { + "access_token": "%s", + "token_type": "Bearer", + "expires_in": 3600 + } + """.formatted(FAKE_JWT)))); + + // 4. Mock Petstore requiring the exact valid JWT + wireMockServer.stubFor(get(urlEqualTo("/v2/pet?petId=99")) + .withHeader("Authorization", equalTo("Bearer " + FAKE_JWT)) + .willReturn(aResponse() + .withHeader("Content-Type", "application/json") + .withBody(""" + { + "id": 99, + "name": "Secure Doggo" + } + """))); + + return Map.of(); + } + + @Override + public void stop() { + if (wireMockServer != null) { + wireMockServer.stop(); + } + } +} diff --git a/docs/modules/ROOT/examples/org/acme/TaskContextWorkflow.java b/docs/modules/ROOT/examples/org/acme/TaskContextWorkflow.java new file mode 100644 index 000000000..6b4339055 --- /dev/null +++ b/docs/modules/ROOT/examples/org/acme/TaskContextWorkflow.java @@ -0,0 +1,31 @@ +package org.acme; + +import static io.serverlessworkflow.fluent.func.dsl.FuncDSL.*; + +import jakarta.enterprise.context.ApplicationScoped; + +import io.quarkiverse.flow.Flow; +import io.serverlessworkflow.api.types.Workflow; +import io.serverlessworkflow.fluent.func.FuncWorkflowBuilder; +import io.serverlessworkflow.impl.TaskContextData; +import io.serverlessworkflow.impl.WorkflowContextData; + +@ApplicationScoped +public class TaskContextWorkflow extends Flow { + @Override + public Workflow descriptor() { + return FuncWorkflowBuilder.workflow("task-context-workflow") + .tasks( + withFilter("taskAudit", + (ExampleEvent payload, + WorkflowContextData workflowContextData, + TaskContextData taskContextData) -> { + // Access the task context + System.out.println("Local Task Name: " + taskContextData.taskName()); + System.out.println("Processing Message: " + payload.eventName()); + + return "Audited [" + payload.eventName() + "] via task: " + taskContextData.taskName(); + }, ExampleEvent.class)) + .build(); + } +} diff --git a/docs/modules/ROOT/examples/test/ConditionalWorkflowTest.java b/docs/modules/ROOT/examples/test/ConditionalWorkflowTest.java new file mode 100644 index 000000000..b9533dc1d --- /dev/null +++ b/docs/modules/ROOT/examples/test/ConditionalWorkflowTest.java @@ -0,0 +1,65 @@ +package test; + +import static com.github.tomakehurst.wiremock.client.WireMock.*; +import static org.junit.jupiter.api.Assertions.assertNotNull; + +import jakarta.inject.Inject; + +import org.acme.ConditionalWorkflow; +import org.acme.ExampleWorkflowsWireMockResource; +import org.acme.ScorePayload; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import com.github.tomakehurst.wiremock.client.WireMock; + +import io.quarkus.test.common.QuarkusTestResource; +import io.quarkus.test.junit.QuarkusTest; +import io.serverlessworkflow.impl.WorkflowModel; + +@QuarkusTest +@QuarkusTestResource(ExampleWorkflowsWireMockResource.class) +public class ConditionalWorkflowTest { + + @Inject + ConditionalWorkflow conditionalWorkflow; + + @BeforeEach + void resetWiremock() { + // Tell the static WireMock client where our mock server actually lives! + WireMock.configureFor(8089); + + // Now it will successfully send the reset command to port 8089 + resetAllRequests(); + } + + @Test + void testApprovedPath() throws Exception { + // 1. Provide a passing score + ScorePayload input = new ScorePayload(85); + + // 2. Execute + WorkflowModel result = conditionalWorkflow.instance(input).start().join(); + + assertNotNull(result, "Workflow should complete successfully"); + + // 3. Verify the engine routed to the 'approve' task and skipped 'reject' + verify(1, postRequestedFor(urlEqualTo("/approve"))); + verify(0, postRequestedFor(urlEqualTo("/reject"))); + } + + @Test + void testRejectedPath() throws Exception { + // 1. Provide a failing score + ScorePayload input = new ScorePayload(60); + + // 2. Execute + WorkflowModel result = conditionalWorkflow.instance(input).start().join(); + + assertNotNull(result, "Workflow should complete successfully"); + + // 3. Verify the engine routed to the 'reject' task and skipped 'approve' + verify(0, postRequestedFor(urlEqualTo("/approve"))); + verify(1, postRequestedFor(urlEqualTo("/reject"))); + } +} diff --git a/docs/modules/ROOT/examples/test/ContextWorkflowTest.java b/docs/modules/ROOT/examples/test/ContextWorkflowTest.java new file mode 100644 index 000000000..36a4fe8dd --- /dev/null +++ b/docs/modules/ROOT/examples/test/ContextWorkflowTest.java @@ -0,0 +1,35 @@ +package test; + +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.is; +import static org.junit.jupiter.api.Assertions.assertNotNull; + +import jakarta.inject.Inject; + +import org.acme.ContextWorkflow; +import org.junit.jupiter.api.Test; + +import io.quarkus.test.junit.QuarkusTest; +import io.serverlessworkflow.impl.WorkflowModel; + +@QuarkusTest +public class ContextWorkflowTest { + + @Inject + ContextWorkflow contextWorkflow; + + @Test + void testContextAwareExecution() throws Exception { + // 1. Provide a standard String payload, as expected by String.class + String input = "Test-Data-123"; + + // 2. Execute the workflow synchronously + WorkflowModel result = contextWorkflow.instance(input).start().join(); + + // 3. Verify the engine completed the execution + assertNotNull(result, "Workflow should complete successfully"); + + assertNotNull(result.asText().orElseThrow()); + assertThat(result.asText().orElseThrow(), is("Processed Test-Data-123")); + } +} diff --git a/docs/modules/ROOT/examples/test/CronWorkflowTest.java b/docs/modules/ROOT/examples/test/CronWorkflowTest.java new file mode 100644 index 000000000..59897104b --- /dev/null +++ b/docs/modules/ROOT/examples/test/CronWorkflowTest.java @@ -0,0 +1,37 @@ +package test; + +import jakarta.inject.Inject; + +import org.acme.CronWorkflow; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Test; + +import io.quarkus.test.junit.QuarkusTest; +import io.serverlessworkflow.impl.WorkflowDefinition; +import io.smallrye.common.annotation.Identifier; + +@QuarkusTest +public class CronWorkflowTest { + + @Inject + CronWorkflow cronWorkflow; + + @Inject + @Identifier("org.acme.CronWorkflow") + WorkflowDefinition cronFlowDefinition; + + @Test + void testCronWorkflow() { + long start = System.currentTimeMillis(); + long elapsed = System.currentTimeMillis() - start; + while (elapsed < 4000) { + elapsed = System.currentTimeMillis() - start; + } + + // Check that the cron in descriptor is not replaced + Assertions.assertSame("* * * * * ?", cronWorkflow.descriptor().getSchedule().getCron()); + + // At least 2 should be run, since the test is using 4 second wait + Assertions.assertTrue(cronFlowDefinition.scheduledInstances().size() > 2); + } +} diff --git a/docs/modules/ROOT/examples/test/EchoYamlWorkflowTest.java b/docs/modules/ROOT/examples/test/EchoYamlWorkflowTest.java index 606990df0..ad97ec26a 100644 --- a/docs/modules/ROOT/examples/test/EchoYamlWorkflowTest.java +++ b/docs/modules/ROOT/examples/test/EchoYamlWorkflowTest.java @@ -4,7 +4,6 @@ import static org.hamcrest.Matchers.is; import java.util.Map; -import java.util.concurrent.TimeUnit; import jakarta.inject.Inject; @@ -20,15 +19,13 @@ class EchoYamlWorkflowTest { @Inject - @Identifier("flow:echo-name") // namespace:name from document section + @Identifier("company:echo-name") // namespace:name from document section WorkflowDefinition definition; @Test void should_echo_name_from_yaml_workflow() throws Exception { - WorkflowModel result = definition.instance(Map.of("name", "Joe")) - .start() - .toCompletableFuture() - .get(5, TimeUnit.SECONDS); + Map input = Map.of("name", "Joe"); + WorkflowModel result = definition.instance(input).start().join(); MatcherAssert.assertThat(result.asMap().orElseThrow().get("message"), is("echo: Joe")); } diff --git a/docs/modules/ROOT/examples/test/EmitWorkflowTest.java b/docs/modules/ROOT/examples/test/EmitWorkflowTest.java new file mode 100644 index 000000000..5fb1067fb --- /dev/null +++ b/docs/modules/ROOT/examples/test/EmitWorkflowTest.java @@ -0,0 +1,65 @@ +package test; + +import static org.junit.jupiter.api.Assertions.*; + +import java.util.Map; + +import jakarta.enterprise.inject.Any; +import jakarta.inject.Inject; + +import org.acme.EmitWorkflow; +import org.junit.jupiter.api.Test; + +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.ObjectMapper; + +import io.quarkus.test.junit.QuarkusTest; +import io.serverlessworkflow.impl.WorkflowModel; +import io.smallrye.reactive.messaging.memory.InMemoryConnector; +import io.smallrye.reactive.messaging.memory.InMemorySink; + +@QuarkusTest +public class EmitWorkflowTest { + + @Inject + EmitWorkflow emitWorkflow; + + @Inject + @Any + InMemoryConnector connector; + + @Inject + ObjectMapper objectMapper; + + @Test + void testEmitEvent() throws Exception { + // 1. Grab the simulated 'flow-out' sink + InMemorySink sink = connector.sink("flow-out"); + sink.clear(); + + // 2. Provide input data that perfectly maps to the fields of Message.class + Map input = Map.of( + "message", "placed"); + + // 3. Execute the workflow synchronously + WorkflowModel result = emitWorkflow.instance(input).start().join(); + + assertNotNull(result, "Workflow should complete successfully"); + + // 4. Verify the sink received the event + var receivedMessages = sink.received(); + assertEquals(1, receivedMessages.size(), "Should have emitted exactly one event"); + + // 5. Unpack the byte array into a JSON Node + byte[] payload = receivedMessages.get(0).getPayload(); + JsonNode eventNode = objectMapper.readTree(payload); + + // 6. Assert standard CloudEvent headers + assertEquals("com.petstore.order.placed.v1", eventNode.get("type").asText()); + assertNotNull(eventNode.get("source"), "Engine should auto-generate a source"); + + // 7. Assert the strongly-typed data block matches our Message structure + JsonNode dataNode = eventNode.get("data"); + assertEquals("placed", dataNode.get("message").asText()); + } +} diff --git a/docs/modules/ROOT/examples/test/ForEachWorkflowTest.java b/docs/modules/ROOT/examples/test/ForEachWorkflowTest.java new file mode 100644 index 000000000..8287a31a4 --- /dev/null +++ b/docs/modules/ROOT/examples/test/ForEachWorkflowTest.java @@ -0,0 +1,56 @@ +package test; + +import static com.github.tomakehurst.wiremock.client.WireMock.*; +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.is; +import static org.junit.jupiter.api.Assertions.assertNotNull; + +import java.util.List; + +import jakarta.inject.Inject; + +import org.acme.ExampleWorkflowsWireMockResource; +import org.acme.ForEachWorkflow; +import org.acme.Order; +import org.acme.OrdersPayload; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import com.github.tomakehurst.wiremock.client.WireMock; + +import io.quarkus.test.common.QuarkusTestResource; +import io.quarkus.test.junit.QuarkusTest; +import io.serverlessworkflow.impl.WorkflowModel; + +@QuarkusTest +@QuarkusTestResource(ExampleWorkflowsWireMockResource.class) +public class ForEachWorkflowTest { + + @Inject + ForEachWorkflow forEachWorkflow; + + @BeforeEach + void resetWiremock() { + WireMock.configureFor(8089); + resetAllRequests(); + } + + @Test + void testForEachIteration() { + OrdersPayload input = new OrdersPayload(List.of( + new Order("ORD-001"), + new Order("ORD-002"), + new Order("ORD-003"))); + + // 2. Execute the workflow synchronously + WorkflowModel result = forEachWorkflow.instance(input).start().join(); + + assertNotNull(result, "Workflow should complete successfully"); + + // 3. Verify the engine looped and executed the HTTP task exactly 3 times! + verify(3, postRequestedFor(urlEqualTo("/process-order"))); + + // 4. Only the result of last task can should be in result + assertThat(result.asText().orElseThrow(), is("{\"processed_orders_status\":\"success\"}")); + } +} diff --git a/docs/modules/ROOT/examples/test/HelloWorkflowTest.java b/docs/modules/ROOT/examples/test/HelloWorkflowTest.java index 97633936c..65e472566 100644 --- a/docs/modules/ROOT/examples/test/HelloWorkflowTest.java +++ b/docs/modules/ROOT/examples/test/HelloWorkflowTest.java @@ -5,7 +5,6 @@ import static org.hamcrest.Matchers.is; import java.util.Map; -import java.util.concurrent.TimeUnit; import jakarta.inject.Inject; @@ -22,11 +21,8 @@ class HelloWorkflowTest { HelloWorkflow workflow; @Test - void should_produce_hello_message() throws Exception { - WorkflowModel result = workflow.instance(Map.of()) - .start() // <2> - .toCompletableFuture() - .get(5, TimeUnit.SECONDS); + void should_produce_hello_message() { + WorkflowModel result = workflow.instance(Map.of()).start().join(); // assuming the workflow writes {"message":"hello world!"} assertThat(result.asMap().orElseThrow().get("message"), is("hello world!")); diff --git a/docs/modules/ROOT/examples/test/HttpOauth2WorkflowTest.java b/docs/modules/ROOT/examples/test/HttpOauth2WorkflowTest.java new file mode 100644 index 000000000..129174db8 --- /dev/null +++ b/docs/modules/ROOT/examples/test/HttpOauth2WorkflowTest.java @@ -0,0 +1,43 @@ +package test; + +import static org.junit.jupiter.api.Assertions.*; + +import java.util.Map; + +import jakarta.inject.Inject; + +import org.acme.HttpOauth2Workflow; +import org.acme.SecureWireMockResource; +import org.junit.jupiter.api.Test; + +import com.fasterxml.jackson.databind.JsonNode; + +import io.quarkus.test.common.QuarkusTestResource; +import io.quarkus.test.junit.QuarkusTest; +import io.serverlessworkflow.impl.WorkflowModel; + +@QuarkusTest +@QuarkusTestResource(SecureWireMockResource.class) +public class HttpOauth2WorkflowTest { + + @Inject + HttpOauth2Workflow httpOauth2Workflow; + + @Test + void testOAuth2SecuredCall() { + // 1. Map input to resolve {petId} + Map input = Map.of("petId", 99); + + // 2. Execute workflow + WorkflowModel result = httpOauth2Workflow.instance(input).start().join(); + + // 3. Extract the native Jackson JsonNode + JsonNode rootNode = result.as(JsonNode.class) + .orElseThrow(() -> new IllegalStateException("Workflow result is empty")); + + // 4. Validate the engine successfully fetched and utilized the token + assertFalse(rootNode.isEmpty(), "The result state should not be empty"); + assertEquals(99, rootNode.get("id").asInt()); + assertEquals("Secure Doggo", rootNode.get("name").asText()); + } +} diff --git a/docs/modules/ROOT/examples/test/HttpWorkflowTest.java b/docs/modules/ROOT/examples/test/HttpWorkflowTest.java new file mode 100644 index 000000000..f23b4a065 --- /dev/null +++ b/docs/modules/ROOT/examples/test/HttpWorkflowTest.java @@ -0,0 +1,45 @@ +package test; + +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import java.util.List; +import java.util.Map; + +import jakarta.inject.Inject; + +import org.acme.ExampleWorkflowsWireMockResource; +import org.acme.HttpWorkflow; +import org.junit.jupiter.api.Test; + +import io.quarkus.test.common.QuarkusTestResource; +import io.quarkus.test.junit.QuarkusTest; +import io.serverlessworkflow.impl.WorkflowModel; + +@QuarkusTest +@QuarkusTestResource(ExampleWorkflowsWireMockResource.class) +public class HttpWorkflowTest { + + @Inject + HttpWorkflow httpWorkflow; + + @Test + void testHttpWorkflow() { + Map input = Map.of("searchQuery", "luke", + "acceptHeaderValue", "application/json"); + + WorkflowModel result = httpWorkflow.instance(input).start().join(); + + assertTrue((Integer) result.asMap().orElseThrow().get("count") > 0, + "Should find at least one character"); + + List> characters = (List>) result.asMap().orElseThrow().get("results"); + assertFalse(characters.isEmpty(), + "The results list should not be empty"); + + String firstCharacterName = (String) characters.get(0).get("name"); + assertTrue(firstCharacterName.toLowerCase().contains("luke"), + "The returned character name should contain 'luke'"); + + } +} diff --git a/docs/modules/ROOT/examples/test/ListenWorkflowTest.java b/docs/modules/ROOT/examples/test/ListenWorkflowTest.java new file mode 100644 index 000000000..ce426d8c1 --- /dev/null +++ b/docs/modules/ROOT/examples/test/ListenWorkflowTest.java @@ -0,0 +1,69 @@ +package test; + +import static org.junit.jupiter.api.Assertions.assertNotNull; + +import java.nio.charset.StandardCharsets; +import java.util.Map; +import java.util.UUID; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.TimeUnit; + +import jakarta.enterprise.inject.Any; +import jakarta.inject.Inject; + +import org.acme.ExampleWorkflowsWireMockResource; +import org.acme.ListenWorkflow; +import org.junit.jupiter.api.Test; + +import io.quarkus.test.common.QuarkusTestResource; +import io.quarkus.test.junit.QuarkusTest; +import io.serverlessworkflow.impl.WorkflowModel; +import io.smallrye.reactive.messaging.memory.InMemoryConnector; +import io.smallrye.reactive.messaging.memory.InMemorySource; + +@QuarkusTest +@QuarkusTestResource(ExampleWorkflowsWireMockResource.class) +public class ListenWorkflowTest { + + @Inject + ListenWorkflow listenWorkflow; + + // Inject the SmallRye In-Memory bridge + @Inject + @Any + InMemoryConnector connector; + + @Test + void testListenForEvent() throws Exception { + var workflowInstance = listenWorkflow.instance(Map.of()); + String instanceId = workflowInstance.id(); + + CompletableFuture futureResult = workflowInstance.start(); + + // 2. Grab the simulated 'flow-in' channel + InMemorySource source = connector.source("flow-in"); + + // 3. Construct a standard CloudEvent JSON payload + String cloudEventJson = """ + { + "specversion": "1.0", + "id": "%s", + "source": "test-framework", + "type": "race.started.v1", + "flowinstanceid": "%s", + "datacontenttype": "application/json", + "data": { + "track": "Monaco" + } + } + """.formatted(UUID.randomUUID().toString(), instanceId); + + // 4. Fire the event into the channel! + source.send(cloudEventJson.getBytes(StandardCharsets.UTF_8)); + + // 3. Block and wait for the workflow to wake up, call WireMock, and finish + WorkflowModel result = futureResult.get(1, TimeUnit.SECONDS); + + assertNotNull(result, "Workflow should successfully complete after being awakened by the event"); + } +} diff --git a/docs/modules/ROOT/examples/test/OpenApiWorkflowTest.java b/docs/modules/ROOT/examples/test/OpenApiWorkflowTest.java new file mode 100644 index 000000000..abee2fde0 --- /dev/null +++ b/docs/modules/ROOT/examples/test/OpenApiWorkflowTest.java @@ -0,0 +1,47 @@ +package test; + +import static org.junit.jupiter.api.Assertions.*; + +import java.util.Map; + +import jakarta.inject.Inject; + +import org.acme.ExampleWorkflowsWireMockResource; +import org.acme.OpenApiWorkflow; +import org.junit.jupiter.api.Test; + +import com.fasterxml.jackson.databind.JsonNode; + +import io.quarkus.test.common.QuarkusTestResource; +import io.quarkus.test.junit.QuarkusTest; +import io.serverlessworkflow.impl.WorkflowModel; + +@QuarkusTest +@QuarkusTestResource(ExampleWorkflowsWireMockResource.class) +public class OpenApiWorkflowTest { + + @Inject + OpenApiWorkflow openApiWorkflow; + + @Test + void testOpenApiWorkflow() { + // Execute workflow (no inputs required for this hardcoded example) + WorkflowModel result = openApiWorkflow.instance(Map.of()).start().join(); + + // 2. Unpack the state directly as a Jackson JsonNode + JsonNode rootNode = result.as(JsonNode.class) + .orElseThrow(() -> new IllegalStateException("Workflow result is empty")); + + // 3. Assertions using the Jackson Tree Model + assertTrue(rootNode.isArray(), "The result state should be a JSON Array"); + assertEquals(1, rootNode.size(), "Should return exactly 1 mocked pet"); + + // Extract the first object in the array + JsonNode firstPet = rootNode.get(0); + + // Use Jackson's .asInt() and .asText() for safe assertions + assertEquals(101, firstPet.get("id").asInt()); + assertEquals("Mocked Doggo", firstPet.get("name").asText()); + assertEquals("available", firstPet.get("status").asText()); + } +} diff --git a/docs/modules/ROOT/examples/test/ParallelWorkflowTest.java b/docs/modules/ROOT/examples/test/ParallelWorkflowTest.java new file mode 100644 index 000000000..8f7f88457 --- /dev/null +++ b/docs/modules/ROOT/examples/test/ParallelWorkflowTest.java @@ -0,0 +1,43 @@ +package test; + +import static com.github.tomakehurst.wiremock.client.WireMock.*; +import static org.junit.jupiter.api.Assertions.assertNotNull; + +import jakarta.inject.Inject; + +import org.acme.ExampleWorkflowsWireMockResource; +import org.acme.ParallelWorkflow; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import com.github.tomakehurst.wiremock.client.WireMock; + +import io.quarkus.test.common.QuarkusTestResource; +import io.quarkus.test.junit.QuarkusTest; +import io.serverlessworkflow.impl.WorkflowModel; + +@QuarkusTest +@QuarkusTestResource(ExampleWorkflowsWireMockResource.class) +public class ParallelWorkflowTest { + + @Inject + ParallelWorkflow parallelWorkflow; + + @BeforeEach + void resetWiremock() { + WireMock.configureFor(8089); + resetAllRequests(); + } + + @Test + void testParallelWorkflowBranchesExecute() { + // 1. Start the workflow + WorkflowModel result = parallelWorkflow.instance().start().join(); + + assertNotNull(result, "Workflow should complete successfully after joining parallel branches"); + + // 2. Verify both endpoints were called exactly once! + verify(1, postRequestedFor(urlEqualTo("/inventory-check"))); + verify(1, postRequestedFor(urlEqualTo("/credit-check"))); + } +} diff --git a/docs/modules/ROOT/examples/test/ParentWorkflowTest.java b/docs/modules/ROOT/examples/test/ParentWorkflowTest.java new file mode 100644 index 000000000..e920e66e9 --- /dev/null +++ b/docs/modules/ROOT/examples/test/ParentWorkflowTest.java @@ -0,0 +1,34 @@ +package test; + +import static org.junit.jupiter.api.Assertions.assertNotNull; + +import java.util.Map; + +import jakarta.inject.Inject; + +import org.acme.ParentWorkflow; +import org.junit.jupiter.api.Test; + +import io.quarkus.test.junit.QuarkusTest; +import io.serverlessworkflow.impl.WorkflowModel; + +@QuarkusTest +public class ParentWorkflowTest { + + @Inject + ParentWorkflow parentWorkflow; + + @Test + void testParentOrchestratesChildrenSuccessfully() throws Exception { + // Define input, so that HttpWorkflow invoked as subflows has necesarry data + Map input = Map.of("searchQuery", "luke", + "acceptHeaderValue", "application/json"); + + // Execute the Parent workflow + WorkflowModel result = parentWorkflow.instance(input) + .start().join(); + + // Verify the execution finished without throwing subflow-resolution exceptions + assertNotNull(result, "Parent workflow should successfully orchestrate and complete"); + } +} diff --git a/docs/modules/ROOT/examples/test/TaskContextWorkflowTest.java b/docs/modules/ROOT/examples/test/TaskContextWorkflowTest.java new file mode 100644 index 000000000..ea88d45a5 --- /dev/null +++ b/docs/modules/ROOT/examples/test/TaskContextWorkflowTest.java @@ -0,0 +1,35 @@ +package test; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; + +import jakarta.inject.Inject; + +import org.acme.ExampleEvent; +import org.acme.TaskContextWorkflow; +import org.junit.jupiter.api.Test; + +import io.quarkus.test.junit.QuarkusTest; +import io.serverlessworkflow.impl.WorkflowModel; + +@QuarkusTest +public class TaskContextWorkflowTest { + + @Inject + TaskContextWorkflow taskContextWorkflow; + + @Test + void testTaskAndWorkflowContext() throws Exception { + ExampleEvent input = new ExampleEvent("System Boot Sequence"); + + WorkflowModel result = taskContextWorkflow.instance(input) + .start().join(); + + assertNotNull(result, "Workflow should complete successfully"); + + // Verify the task context data was successfully appended + String finalState = result.asText().orElseThrow(); + assertEquals("Audited [System Boot Sequence] via task: taskAudit", finalState, + "The engine should inject the task name into the lambda execution"); + } +} diff --git a/docs/modules/ROOT/nav.adoc b/docs/modules/ROOT/nav.adoc index 0db0c5b78..5c965af48 100644 --- a/docs/modules/ROOT/nav.adoc +++ b/docs/modules/ROOT/nav.adoc @@ -43,5 +43,6 @@ ** xref:dsl-cheatsheet.adoc[Java DSL cheatsheet] ** xref:reference-initialization.adoc[Initialization Process] ** xref:quarkus-flow-cookbook.adoc[] +** xref:quarkus-flow-java-cookbook.adoc[] ** xref:configuration.adoc[Extension Configuration Reference] ** xref:performance-benchmarks.adoc[Performance Benchmark] diff --git a/docs/modules/ROOT/pages/quarkus-flow-java-cookbook.adoc b/docs/modules/ROOT/pages/quarkus-flow-java-cookbook.adoc new file mode 100644 index 000000000..a92944a86 --- /dev/null +++ b/docs/modules/ROOT/pages/quarkus-flow-java-cookbook.adoc @@ -0,0 +1,155 @@ += Quarkus Flow & CNCF Serverless Workflow Java Cookbook +:sectnums: +:page-role: reference +:examples-dir: ../main/java +include::includes/attributes.adoc[] + + +This cookbook provides a comprehensive collection of practical, copy-and-paste examples for building modern workflows using the Serverless Workflow Java SDK Fluent API. + +All workflows in Quarkus Flow are defined as CDI beans by extending the `Flow` class and overriding the `descriptor()` method. + +We recommend using the `FuncWorkflowBuilder` with `FuncDSL`. + +:sectnums!: +== Essential Setup +Every workflow should follow this structure. + +[source,java] +---- +package org.acme; + +import io.quarkiverse.flow.Flow; +import io.serverlessworkflow.api.types.Workflow; +import io.serverlessworkflow.fluent.func.spec.FuncWorkflowBuilder; +import jakarta.enterprise.context.ApplicationScoped; + +// Recommended Static Imports +import static io.serverlessworkflow.fluent.func.dsl.FuncDSL.*; + +@ApplicationScoped +public class MyWorkflow extends Flow { + @Override + public Workflow descriptor() { + return FuncWorkflowBuilder.workflow("my-workflow-id") + .tasks( + // tasks go here + ) + .build(); + } +} +---- + +:sectnums: +[#scheduled_execution] +== Scheduled Executions (CRON) +Trigger workflows on a schedule by defining a `schedule` within the builder. + +Check how to xref:scheduler.adoc[schedule workflow executions] for more details. + +[source,java] +---- +include::{examples-dir}/org/acme/CronWorkflow.java[] +---- + +[#external_api_interaction] +== External API Integrations (HTTP REST) +Use the `call` task with `http` to interact with REST endpoints, managing headers and query parameters dynamically. + +[source,java] +---- +include::{examples-dir}/org/acme/HttpWorkflow.java[] +---- + +[#openapi_invocation] +== OpenAPI Services +Invoke OpenAPI endpoints by referencing the specification and the `operationId`. + +[source,java] +---- +include::{examples-dir}/org/acme/OpenApiWorkflow.java[] +---- + +[#authentication] +== Authentication (OAuth2) +Define authentication mechanisms for secure outbound communication. + +[source,java] +---- +include::{examples-dir}/org/acme/HttpOauth2Workflow.java[] +---- + +[#events] +== Event Consumption and Production +To integrate the workflow with CloudEvents. Use `listen` to pause for events and `emit` to publish them. + +Listening for an Event: +[source,java] +---- +include::{examples-dir}/org/acme/ListenWorkflow.java[] +---- + +Emitting an Event: +[source,java] +---- +include::{examples-dir}/org/acme/EmitWorkflow.java[] +---- + +[#conditional_logic] +== Conditional Logic +Branch execution based on data state using `switchCase(...)`, `switchWhen(...)` and `switchWhenOrElse(...)``. + +[source,java] +---- +include::{examples-dir}/org/acme/ConditionalWorkflow.java[] +---- + +[#iteration] +== Iteration +Execute logic over collections of data using the `forEach(...)`. + +[source,java] +---- +include::{examples-dir}/org/acme/ForEachWorkflow.java[] +---- + +[#subflows] +== Invoking Subflows +Execute another workflow using `subflow(...)` + +Ensure the `name`, `namespace` and `version` parameters are matching the referenced workflows. + +[source,java] +---- +include::{examples-dir}/org/acme/ParentWorkflow.java[] +---- + +[#parallel] +== Parallel Execution (`fork`) +Execute independent branches simultaneously to reduce total execution time. + +Using `fork(...)` +[source,java] +---- +include::{examples-dir}/org/acme/ParallelWorkflow.java[] +---- + +[#with_context] +== Context-aware execution +Execute workflow with access to engine metadata using `withContext(...)` + +[source,java] +---- +include::{examples-dir}/org/acme/ContextWorkflow.java[] +---- + +Execute workflow with access to task metadata using `withFilter(...)` +[source,java] +---- +include::{examples-dir}/org/acme/TaskContextWorkflow.java[] +---- + +:sectnums!: +== See also + +* xref:dsl-cheatsheet.adoc[] — a quick cheatsheet to Java DSL. \ No newline at end of file diff --git a/docs/pom.xml b/docs/pom.xml index 2bd7ff8c6..cacdf2d45 100644 --- a/docs/pom.xml +++ b/docs/pom.xml @@ -55,6 +55,24 @@ assertj-core runtime + + io.quarkiverse.flow + quarkus-flow-scheduler + ${project.version} + + + org.wiremock + wiremock + compile + + + io.smallrye.reactive + smallrye-reactive-messaging-in-memory + + + io.quarkus + quarkus-messaging + diff --git a/docs/src/main/resources/application.properties b/docs/src/main/resources/application.properties index e61e2c86f..7e257e6bc 100644 --- a/docs/src/main/resources/application.properties +++ b/docs/src/main/resources/application.properties @@ -1,2 +1,15 @@ quarkus.flow.definitions.dir=modules/ROOT/examples/flow -quarkus.langchain4j.ollama.devservices.enabled=false \ No newline at end of file +quarkus.langchain4j.ollama.devservices.enabled=false + +org.acme.agentic.market-data.url=http://localhost:${quarkus.http.port}/market-data/{ticker} + +quarkus.scheduler.enabled=true +quarkus.scheduler.cron-type=quartz + +quarkus.flow.messaging.defaults-enabled=true + +# Example messaging setup - tells the compiled bridge to route traffic to local memory for tests +mp.messaging.incoming.flow-in.connector=smallrye-in-memory +mp.messaging.outgoing.flow-out.connector=smallrye-in-memory + +notification.service.base-url=http://localhost:${quarkus.http.port} \ No newline at end of file